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."
- What it is:
cronddaemon 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 * * 1Monday 2am,*/15 * * * *every 15 min,0 9-17 * * 1-5business 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:/binfirst, absolute paths only, log via>> /var/log/job.log 2>&1orlogger -t cron, lock viaflock -n /tmp/lockto 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.
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
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)
| Expression | When | Use |
|---|---|---|
* * * * * | Every minute | Health check (ensure < 60s runtime + flock) |
0 * * * * | Top of every hour (:00) | Hourly report |
0 2 * * * | 2:00am daily | Nightly backup (low traffic) |
0 2 * * 1 | 2:00am Monday | Weekly Monday job |
0 0 1 * * | Midnight 1st of month | Monthly billing |
0 9-17 * * 1-5 | 9am-5pm hourly weekdays | Business hours sync |
*/15 * * * * | Every 15 min | Polling |
30 2 * * * | 2:30am daily | Stagger from 2:00 crowd (avoid thundering herd) |
0 0 1 1 * | Jan 1 midnight | Yearly |
@reboot | At boot | Start 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
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.
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
| Symptom | Cause → Fix |
|---|---|
| command not found | PATH 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 next | Use 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 log | Add >> /var/log/job.log 2>&1 or logger -t job — cron only emails if MAILTO+MTA |
| 2:30am skipped in March | DST spring gap → run UTC or choose 3:30am after DST transition window |
# 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:
- Logging: Cron's minimal env means
/var/log/cronshowsCRON[pid]: (user) CMD (command)but not stdout. Your command must log:>> /var/log/myjob.log 2>&1(append, both stdout+stderr) pluslogger -t myjobfor syslog searchjournalctl -t myjob. Rotate vialogrotate— cron log without rotation fills disk. See cron(8) logging section. - Mail: Top of crontab
MAILTO=alerts@example.com→ cron emails output ifsendmailconfigured; on cloud VMs without MTA, setMAILTO=""to suppress and rely on file log. Test mail withecho test | mail -s test $USER. - Locking: If job may exceed interval (e.g., 5min job but
*/5 * * * *every 5min), useflock -n /tmp/myjob.lock -c '/usr/local/bin/myjob.sh'—-nnon-blocking exits if lock held, preventing overlap queue. Alternativerun-oneor pgrep guard.flockis 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,5Mon/Wed/Fri at 2am. - 59-minute trap:
* * * * * sleep 30; /jobattempts every 30s but cron granularity is 1 min — use loop or systemdOnUnitActiveSec=30sif 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.
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
- Does cron daemon run?
systemctl status cron(Debian) orcrond(RHEL) —active (running)? If not,sudo systemctl enable --now cron. - Did cron see file?
grep CRON /var/log/syslogorgrep CRON /var/log/cronon next minute — you should seeCRON[1234]: (user) CMD (command). No line → cron not installed or crontab not saved (did you save in editor?). - 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. - Is script executable and has shebang?
chmod +x /usr/local/bin/backup.shand first line#!/bin/bash -e— cron usesSHELLbut script without +x viabash /pathworks; 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).
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.