All Tools View Categories Blog About Contact Privacy

Cron Jobs Explained: The Complete Guide to Scheduling Tasks

Cron Jobs Explained: The Complete Guide to Scheduling Tasks

Cron is Unix's time-based scheduler — a line 0 2 * * * /usr/local/bin/backup.sh runs that command at 2:00am daily, forever, whether you're logged in or not. This complete guide explains how cron jobs work, decodes the 5-field syntax with 12 real examples, shows how to edit crontabs safely, handle time zones and DST, and avoid the 3 pitfalls that cause 90% of "cron didn't run."

TL;DR — Cron Jobs:
  • What it is: crond daemon checks every minute; if time matches a crontab line * * * * * command, it runs that command with a minimal env (tiny PATH, no shell init).
  • Syntax: Minute 0-59 | Hour 0-23 | Day 1-31 | Month 1-12 | Weekday 0-7 (0/7 Sunday) | command* every, , list, - range, / step. Build without memorizing via our cron generator (pick everyday/weekday → emits correct 5 fields).
  • 12 examples: 0 * * * * hourly, 0 2 * * * 2am daily, 0 2 * * 1 Monday 2am, */15 * * * * every 15 min, 0 9-17 * * 1-5 business hours weekdays — verify before save with our cron expression tester (shows next 5 runs: Mar 10 02:00, 02:15...).
  • Time zones: cron runs in server TZ (usually UTC, not your laptop). 2:30am local on DST spring day may not exist → skip; fall has double 1am. Run servers in UTC and display locally; plan global 9am NY/London/Tokyo with our cron global schedule planner to see NY 9am = 14:00 UTC vs EDT 13:00 UTC.
  • Reliability: PATH=/usr/local/bin:/usr/bin:/bin first, absolute paths only, log via >> /var/log/job.log 2>&1 or logger -t cron, lock via flock -n /tmp/lock to prevent overlap.

What Are Cron, Crontab, and Cron Jobs — Who Does What

cron is the time-based job scheduler present since Unix V7 (1975) — cron(8). crond is the daemon that sleeps until the next minute, checks its crontabs, and executes any whose time matches now. crontab (cron table) is the file listing jobs — per-user files in /var/spool/cron/crontabs/$USER managed via crontab command, plus system files /etc/crontab and /etc/cron.d/* which include a user field. cron job is one line: schedule + command. See crontab(5) format.

What is cron crontab cron job daemon vs table

User crontab (your jobs): * * * * * /usr/local/bin/backup.sh — 5 fields then command. System crontab: * * * * * root /usr/local/bin/cleanup.sh — extra user field before command. Never edit files directly; crontab -e checks syntax and installs via daemon.

System Directories — /etc/cron.*

Many distros add /etc/cron.hourly/*, daily, weekly executed via system crontab's run-parts. Your line 0 2 * * * is explicit; @daily alias is equivalent but coarser.

Syntax — 5 Fields, Operators, and Special Strings You Can Trust

# ┌─ minute 0-59      ┌─ month 1-12
# │ ┌─ hour 0-23      │ ┌─ command
# │ │ ┌─ day 1-31     │ │
# * * * * *  command
# │ │ │ │ └─ weekday 0-7 (0 and 7 = Sun)
# │ │ └─ day 1-31
# └─ hour 0-23
0 2 * * *  /usr/local/bin/backup.sh   # 02:00 daily
Cron 5 fields minute hour day month weekday operators special strings

Operators:

  • * every — * * * * * every minute (use sparingly).
  • , list — 0,15,30,45 * * * * every quarter hour (same as */15).
  • - range — 0 9-17 * * * hourly 9am-5pm.
  • / step — */15 * * * * every 15 min; 0 */2 * * * every 2 hours on the hour; 0 2-14/2 * * * every 2h 2am-2pm.

Special strings (non-standard, widely supported on Vixie cron): @reboot at boot, @yearly (@annually) 0 0 1 1 *, @monthly 0 0 1 * *, @weekly 0 0 * * 0, @daily (@midnight) 0 0 * * *, @hourly 0 * * * *. Not POSIX — test via man 5 crontab on your system; Docker's Alpine cron may differ. Reference: crontab(5).

Day and Weekday Are OR — The Most Misread Rule

0 2 15 * 1 does not mean "15th that is Monday" — it means "15th of month OR every Monday" (two conditions). Next run is the sooner of next 15th or Monday 2am. To mean AND, use 0 2 * * 1 plus check day inside script: [ $(date +%d) -eq 15 ] || exit 0 (note % escaped in cron).

12 Real Examples — Copy-Paste Ready (Verified via Tester)

12 cron examples every minute hourly daily weekly monthly
ExpressionWhenUse
* * * * *Every minuteHealth check (ensure < 60s runtime + flock)
0 * * * *Top of every hour (:00)Hourly report
0 2 * * *2:00am dailyNightly backup (low traffic)
0 2 * * 12:00am MondayWeekly Monday job
0 0 1 * *Midnight 1st of monthMonthly billing
0 9-17 * * 1-59am-5pm hourly weekdaysBusiness hours sync
*/15 * * * *Every 15 minPolling
30 2 * * *2:30am dailyStagger from 2:00 crowd (avoid thundering herd)
0 0 1 1 *Jan 1 midnightYearly
@rebootAt bootStart service (use systemd for prod)

Build your own without memorizing 0-59 vs 0-23 via our cron generator — click "Every weekday at 9am" → emits 0 9 * * 1-5 — and confirm via our cron expression tester which shows next 5 fire times (e.g., Mon Mar 10 09:00, Tue...).

Stagger to Avoid Thundering Herd

Don't schedule 100 servers at 0 2 * * * — they hit DB at same second. Stagger via 30 2 * * * or random per-host $(($RANDOM % 60)) 2 * * * via generate-once per host.

Crontab — View, Edit, and Log (Never Edit File Directly)

crontab -e      # edit (uses $EDITOR, default nano/vi)
crontab -l      # list
crontab -r      # remove — dangerous, confirms none, add backup first: crontab -l > ~/crontab.bak
crontab -u www-data -e  # other user (as root)
cat /etc/crontab  # system, needs user field: * * * * * root /usr/local/bin/cleanup.sh
Crontab edit list remove PATH mailto logging

Cron's env is minimal: PATH=/usr/bin:/bin (no /usr/local/bin), no .bashrc, no NVM. Fix: first lines of crontab: PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin and SHELL=/bin/bash if you use bashisms. Use absolute paths: /usr/local/bin/backup.sh not backup.sh. Docs: crontab(5) env section.

Logging — Cron Is Silent by Default

Cron doesn't auto-log command output — you add it: 0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 or pipe to syslog | logger -t backup. Add MAILTO=user@example.com at top → cron emails stdout/stderr if mail configured; many cloud VMs have no MTA — check or log to file.

Time Zones and Daylight Saving — UTC Safer, Global Needs Planner

Cron runs in system TZ (/etc/timezone or TZ env) — usually UTC on servers, not your laptop America/New_York. 0 2 * * * on UTC server is 2am UTC = 9pm previous day EST (EST is UTC-5). On DST spring forward, local 2:00–2:59 does not exist — 30 2 * * * that day is skipped; fall back has double 1am — 30 1 * * * fires twice. Vixie cron supports per-crontab CRON_TZ=America/New_York but not all — check man 5 crontab on host; portable is to set server TZ=UTC and keep crons in UTC, converting user display.

Time zones UTC vs local DST spring skip global planner

Global teams: "9am NY daily" = 14:00 UTC (EST) but 13:00 UTC (EDT) — one cron can't be both without TZ-aware scheduler. Solutions: two lines (0 14 * * * winter + 0 13 * * * summer with date guard), or use our cron global schedule planner — pick NY 9am, London 2pm, Tokyo 10pm, it shows the UTC cron pair plus DST transitions, so you run correctly year-round.

Best Practice — Run Everything in UTC

Keep server TZ=UTC, crons written for UTC, app converts to user TZ for display — no skip/double. If business requires 9am NY, set CRON_TZ=America/New_York only if your cron supports it (test via a * * * * * date >> /tmp/tz.log log).

Pitfalls — Why "Cron Didn't Run" and the Fix for Each

SymptomCause → Fix
command not foundPATH tiny → add PATH=/usr/local/bin:... at top or use full path /usr/local/bin/node not node
% in command → truncated% is newline to cron → escape % or wrap in script; e.g., date +%Y-%m-%d
Last run overlapping nextUse lock: /usr/bin/flock -n /tmp/job.lock /usr/local/bin/job.sh-n non-blocking, skips if locked; see flock(1)
Silent fail, no logAdd >> /var/log/job.log 2>&1 or logger -t job — cron only emails if MAILTO+MTA
2:30am skipped in MarchDST spring gap → run UTC or choose 3:30am after DST transition window
Pitfalls PATH percent lock flock test before save
# Production pattern: PATH + flock + log
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
SHELL=/bin/bash
MAILTO=""
0 2 * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Verify before crontab -e with tester (next runs) — typo 0 2 * * 1-6 intended 1-5 is caught by tester, not by crontab install (most crons accept it). See Debian cron howto.

Security and Permissions

Cron jobs run as crontab owner — don't run heavy as root if not needed; create dedicated user www-data via crontab -u www-data -e. System crontab /etc/crontab requires user field — user crontab forbids it. Check /etc/cron.allow and /etc/cron.deny for who may use cron per crontab(1).

anacron vs Cron vs Systemd Timers — Which Scheduler When?

Cron assumes machine is always on. If your laptop/desktop sleeps at 2am, that 0 2 * * * backup is missed forever. anacron (configured in /etc/anacrontab, e.g., 1 5 cron.daily nice run-parts /etc/cron.daily) catches up after boot — "run daily, if not run in 1 day, run 5 min after boot." Good for laptops, not servers. systemd timers (OnCalendar=*-*-* 02:00:00) are cron's modern replacement on systemd Linux — they log to journalctl -u myjob, support Persistent=true (like anacron catch-up), AccuracySec staggering, and RandomizedDelaySec. Cron is simpler and portable (every Unix), timers are richer but Linux-only. For servers always on, cron is fine; for laptops, add anacron or timer with Persistent. See anacron(8) and systemd.timer(5).

Editing Safely — Backup, Syntax Check, and Version Control

Never crontab -r without backup — it has no undo. Safe flow:

crontab -l > ~/crontab.bak.$(date +%F)  # backup with date
crontab -e                             # edit, save
crontab -l  # verify list
# Optional git:
mkdir -p ~/cron && crontab -l > ~/cron/crontab.$(hostname) && git add cron/ && git commit -m "cron: add backup"

Syntax check: crontab -l | crontab - is no-op if valid? Actually install checks. Better: copy line to tester (next 5 runs) before save — 0 2 * * 1-6 typo 1-6 vs 1-5 is caught by tester showing Saturday runs you didn't intend. Many cron implementations silently accept 6 vs 0 for Sunday.

System Crontab Audit

List all: ls -R /etc/cron* && cat /etc/crontab && crontab -l && for u in $(cut -f1 -d: /etc/passwd); do crontab -u $u -l 2>/dev/null; done → you see user, system, and drop-ins. Check /var/log/cron or journalctl -u cron for CMD (user) lines confirming daemon ran — not just your app log.

Logging, Mail, and Locking Deep — Production Pattern

Three add-ons separate dev cron from prod:

  1. Logging: Cron's minimal env means /var/log/cron shows CRON[pid]: (user) CMD (command) but not stdout. Your command must log: >> /var/log/myjob.log 2>&1 (append, both stdout+stderr) plus logger -t myjob for syslog search journalctl -t myjob. Rotate via logrotate — cron log without rotation fills disk. See cron(8) logging section.
  2. Mail: Top of crontab MAILTO=alerts@example.com → cron emails output if sendmail configured; on cloud VMs without MTA, set MAILTO="" to suppress and rely on file log. Test mail with echo test | mail -s test $USER.
  3. Locking: If job may exceed interval (e.g., 5min job but */5 * * * * every 5min), use flock -n /tmp/myjob.lock -c '/usr/local/bin/myjob.sh'-n non-blocking exits if lock held, preventing overlap queue. Alternative run-one or pgrep guard. flock is util-linux, widely available — see flock(1).
# Production pattern — PATH + SHELL + MAILTO + flock + log + lock
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
SHELL=/bin/bash
MAILTO=""
0 2 * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Test: /usr/local/bin/backup.sh must be executable, has shebang #!/bin/bash -e

Advanced Syntax — Step With Range, Lists, and the 59-Minute Trap

  • Step with range: 0 9-17/2 * * * = every 2h 9am,11am,1pm,3pm,5pm — step after range, not after *.
  • List + range: 0 2 * * 1,3,5 Mon/Wed/Fri at 2am.
  • 59-minute trap: * * * * * sleep 30; /job attempts every 30s but cron granularity is 1 min — use loop or systemd OnUnitActiveSec=30s if true 30s needed. */5 * * * * is every 5 minutes, not 5 seconds.

For non-standard seconds, wrap: * * * * * for i in 0 30; do /job & sleep 30; done but prefer systemd timer with OnCalendar=*:*:0/30.

Checklist Before You Save — Copy These 3:
1) Tester: 0 2 * * 1 → next 5 runs Mon 02:00? If not, fix.
2) Manual run: PATH=/usr/local/bin:/usr/bin:/bin /usr/local/bin/backup.sh >> /tmp/test.log 2>&1; echo $?
3) Log tail: tail -f /var/log/backup.log & sleep 65 && ls -l /var/log/backup.log # confirms cron fired
If manual run works but cron doesn't, it's PATH or % escape or permission (script not +x).

Security, Permissions, and Cron Allow/Deny

Cron jobs run as crontab owner — not always root. Create a dedicated backup user, sudo -u backup crontab -e with only /var/backups write, not root with full FS. System crontab /etc/crontab requires user field; user crontab forbids it — * * * * * root /cmd in user crontab fails with bad minute. Check /etc/cron.allow (if exists, only listed may use cron) and /etc/cron.deny (blocked) per crontab(1) — if cron.allow exists and you're not in it, crontab -e says you are not allowed.

Debugging — When Tester Shows Next Runs But Cron Still Silent

  1. Does cron daemon run? systemctl status cron (Debian) or crond (RHEL) — active (running)? If not, sudo systemctl enable --now cron.
  2. Did cron see file? grep CRON /var/log/syslog or grep CRON /var/log/cron on next minute — you should see CRON[1234]: (user) CMD (command). No line → cron not installed or crontab not saved (did you save in editor?).
  3. Does command run outside cron with same env? Simulate: env -i PATH=/usr/bin:/bin SHELL=/bin/sh /usr/local/bin/backup.sh — if this fails with same PATH, cron will too. Add missing PATH first.
  4. Is script executable and has shebang? chmod +x /usr/local/bin/backup.sh and first line #!/bin/bash -e — cron uses SHELL but script without +x via bash /path works; direct path needs +x.

Journal vs Cron Log vs App Log — Three Logs

journalctl -u cron (systemd) shows daemon start/stop; /var/log/cron shows cron's decision to run; /var/log/backup.log (your >> log 2>&1) shows command stdout/stderr. You need all three: daemon → cron → app. If app log empty but cron log shows CMD, command ran but produced no output (add echo $(date): start >> log to script).

Record Keeping — Version Your Crontab

Save crontab -l > ~/cron/crontab.$(hostname).$(date +%F) && git commit — like code, crontab history matters when 3am pager hits and you ask "what changed?" Notepad loses it.

Cron Alternatives — When Cron Is Not Enough

Cron is polling, not event-driven. For triggers like "on file upload" or "queue message", use inotify + systemd.path or message queue workers (SQS, Celery). Cron also lacks dependencies ("run B after A succeeds")—use Airflow or Dkron for DAGs. But for time-based "every night at 2am, no matter what", cron remains the simplest and most portable — it's on every Unix, no agent.

Keep jobs idempotent: if cron fires twice (manual + schedule, or fall DST double), running twice should be safe. Wrap non-idempotent (email send) with lock + database dedup.

Cron is idempotent, time-based, and minimal — keep jobs short, logged, and locked; for DAGs or file triggers, use systemd timers or Airflow, but for every-2am, cron remains the one-liner that survives decades.

Test cron via tester, then manual run with same PATH, then watch grep CRON /var/log/cron next minute — three confirmations before you trust the schedule.

Cron plus health check: pair every cron with curl --fail https://hc-ping.com/uuid at end of script — if cron fails or server sleeps, you get alert, not silent miss. For critical backups, add second cron offset 30 min as watchdog that checks log mtime.

Bonus: run crontab -l > ~/cron.bak. weekly via cron itself — self-backup catches accidental -r before you need it.

Keep crontab versioned — crontab -l > cron.DESKTOP-FM6P69B.bak + git — so history shows who changed what when 3am pager hits.

Frequently Asked Questions

What is a cron job?

A scheduled task defined by a crontab line * * * * * command — 5 time fields plus command — executed by crond daemon when time matches. Examples: backup at 2am daily, report hourly, cleanup weekly.

How do I create a cron job?

Run crontab -e, add a line like 0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1, save. Verify with crontab -l and tester for next fire times. Add PATH at top.

What does * * * * * mean?

Every minute of every hour, day, month, weekday — the "always" schedule. * in a field means every value in its range. 0 2 * * * fixes minute 0 hour 2, others every → 2am daily.

How do I test a cron expression?

Use a tester: paste 0 2 * * 1 → it shows next 5 runs (Mon Mar 10 02:00, Mar 17...). Check locally via crontab -l + manual run of command with same env/PATH.

Does cron use UTC or local time?

Server's system TZ (often UTC). Check timedatectl or cat /etc/timezone. 2am local ≠ 2am UTC — 5h diff EST. Use UTC on servers and convert display; or add CRON_TZ=America/New_York if your cron supports (Vixie).

Why didn't my cron job run?

Top 3: PATH missing → command not found (add PATH), % not escaped → cron truncates (use %), overlapping runs → use flock -n, or DST gap. Add logging >> log 2>&1 and test command manually with same PATH.