Anatomy of classic 5-field cron
Standard Unix cron daemons (such as Vixie cron and cronie) accept schedules formatted as exactly five whitespace-separated fields. Each field governs a specific temporal dimension in strict sequential order:
* * * * * command to execute
│ │ │ │ │
│ │ │ │ └─── Day of week (0 - 7, where 0 and 7 = Sunday, or SUN-SAT)
│ │ │ └───── Month of year (1 - 12 or JAN-DEC)
│ │ └─────── Day of month (1 - 31)
│ └───────── Hour of day (0 - 23, 24-hour military format)
└─────────── Minute of hour (0 - 59) | Field | Time unit | Allowed range | Special characters |
|---|---|---|---|
| 1 | Minute | 0–59 | * , - / |
| 2 | Hour | 0–23 | * , - / |
| 3 | Day of Month | 1–31 | * , - / |
| 4 | Month | 1–12 or JAN–DEC | * , - / |
| 5 | Day of Week | 0–7 (0 or 7 = Sun) or SUN–SAT | * , - / |
Important note on non-standard engines: Frameworks such as Quartz Scheduler, Java Spring @Scheduled, and AWS CloudWatch Events frequently employ 6-field or 7-field formats, adding seconds (0-59) as the first token or year as the final token. Pasting a 6-field expression into a Linux crontab -e causes cron to treat the sixth token as part of the command executable.
Special characters and operators
Cron provides four foundational operators to craft precise recurring schedules:
- Asterisk (
*) — Represents all valid values within a field. The expression* * * * *executes every minute of every hour of every day. - Comma (
,) — Specifies a discrete list of values. For example,15,45 * * * *triggers at minute 15 and minute 45 of each hour. - Hyphen (
-) — Defines an inclusive contiguous range. For instance,0 9-18 * * 1-5runs at the top of every business hour (9:00 AM through 6:00 PM) from Monday through Friday. - Slash (
/) — Dictates step increments over a range. For example,*/10 * * * *triggers every 10 minutes starting at 0 (0, 10, 20, 30, 40, 50). Similarly,5-55/15 * * * *executes at minutes 5, 20, 35, and 50.
The Boolean OR pitfall between Day of Month and Day of Week
One of the most persistent misunderstandings in Unix administration involves setting both field 3 (Day of Month) and field 5 (Day of Week) simultaneously. If both fields contain non-asterisk values, the cron daemon evaluates them using a logical OR, never an AND!
Consider the line: 0 2 13 * 5 /opt/backup.sh. While a human might read this as "run at 2:00 AM on Friday the 13th", cron interprets it as: run on the 13th of the month OR every Friday of the week.
To ensure execution strictly when both calendar conditions coincide (Friday the 13th), shift the weekday validation into the shell command itself:
0 2 13 * * [ $(date +\%u) -eq 5 ] && /opt/scripts/friday13_backup.sh Special macros (@reboot, @daily, @hourly)
Vixie cron and system crontabs support predefined macro strings that substitute for the standard five-field timing parameters:
| Macro | Equivalent | Execution frequency |
|---|---|---|
@reboot | — | Executed once upon cron daemon boot initialization |
@yearly / @annually | 0 0 1 1 * | January 1st at midnight (00:00) once per calendar year |
@monthly | 0 0 1 * * | First day of each month at midnight (00:00) |
@weekly | 0 0 * * 0 | Every Sunday at midnight (00:00) |
@daily / @midnight | 0 0 * * * | Every single day at midnight (00:00) |
@hourly | 0 * * * * | At minute zero of every hour |
The @reboot macro is especially handy for launching developer background workers, docker compose projects, or microservice listeners without requiring root access to author a systemd unit:
@reboot sleep 15 && cd /opt/apps/myproject && docker compose up -d Time zones and Daylight Saving Time (DST) pitfalls
By default, the cron daemon reads the system clock and the host /etc/localtime file. On modern distributions (Debian, Ubuntu, RHEL), you can declare an explicit timezone at the top of your crontab:
CRON_TZ=UTC
# Or an explicit regional zone:
# CRON_TZ=America/New_York
0 3 * * * /opt/scripts/nightly_report.sh If your server operates in a local timezone that observes Daylight Saving Time (DST), seasonal clock adjustments trigger edge-case anomalies:
- Spring forward (e.g. 02:00 to 03:00): The clock jumps over the 2:00 AM hour entirely. Any cron jobs scheduled between 02:00 and 02:59 are skipped and will not run that night.
- Fall back (e.g. 03:00 to 02:00): The 2:00 AM hour occurs twice. Any cron jobs scheduled within that window will run twice unless guarded by an external lock.
For this reason, cloud servers, databases, and cron workers should always be configured to operate in Universal Coordinated Time: timedatectl set-timezone UTC.
7 most common reasons crontab fails
When a shell script executes cleanly in your interactive terminal but refuses to run or silently dies inside crontab, the issue almost universally stems from the stripped-down cron execution environment:
- Stripped PATH environment variable. Interactive logins load comprehensive paths from
/etc/environment,~/.bashrc, and~/.profile. Cron, however, initializes with a bare-bones path:PATH=/usr/bin:/bin. If your script referencesdocker,node,python3, or tools inside/usr/local/bin,/snap/bin, or an nvm/pyenv directory, execution fails with command not found. Explicitly declarePATHat the head of crontab or use absolute binary paths. - Unintended Current Working Directory (CWD). Cron sets the initial working directory to the user's home folder (
$HOME), not the folder where your script resides. Commands likepython main.pyor relative file reads like./config.jsonfail with FileNotFoundError. Always explicitly change directories:cd /opt/app && ./run.sh. - Unescaped percent signs (%). In crontab syntax, a literal percent sign (
%) represents a newline character, and any subsequent tokens are piped to standard input. An expression liketar -czf backup-$(date +%Y-%m-%d).tar.gzfractures into invalid commands. You must escape all percent symbols with backslashes:date +\%Y-\\%m-\\%d. - Default shell is /bin/sh instead of /bin/bash. Cron defaults to
/bin/sh(often dash on Debian/Ubuntu). Bash-specific constructs like double brackets[[ ... ]], associative arrays, or process substitution cause immediate syntax error: unexpected token crashes. Declare your shell explicitly:SHELL=/bin/bash. - Missing execute permissions (chmod +x). Your script file must have executable permissions assigned to the crontab user (
chmod +x /path/to/script.sh) and include a valid shebang line (e.g.,#!/usr/bin/env bashor#!/usr/bin/env python3) at line 1. - Missing trailing newline character. Classic Vixie cron reads files line by line using newline delimiters. If the final line in your crontab lacks a trailing
\n, the daemon silently ignores that entire job entry without throwing an alert. Always leave a blank line at the bottom of crontab files. - Missing environment variables (.env). Your interactive credentials, database connection strings, and runtime flags like
NODE_ENV=productionare absent in cron. Source your environment explicitly:set -a; . /opt/app/.env; set +a; /opt/app/worker.
Logging, debugging and concurrency protection (flock)
By default, cron attempts to deliver failure output through local Unix mail (via sendmail). On modern servers lacking a configured mail transport agent, that output is permanently discarded.
Redirecting stdout and stderr to log files
Capture all runtime diagnostics by piping both standard output (stdout) and standard error (stderr) into a designated log file:
# Append all diagnostics to log file:
0 4 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Discard output entirely for self-logging applications:
0 * * * * /opt/scripts/sync.sh > /dev/null 2>&1 Inspecting system-level cron logs
To verify whether the cron daemon actually fired a scheduled trigger, inspect the host system journal:
# On systemd Linux distributions (Ubuntu, Debian, Fedora, RHEL):
journalctl -u cron.service -e --no-pager
# Or through traditional syslog entries:
grep CRON /var/log/syslog | tail -n 50 Simulating cron's isolated environment
Instead of waiting for an overnight trigger, test your script in a clean sandbox that mimics cron's stripped environment with a single terminal command:
env -i HOME="$HOME" PATH="/usr/bin:/bin" SHELL="/bin/sh" /path/to/my_script.sh Preventing overlapping jobs with flock
When a job scheduled every minute encounters a slow external database query or network lag, subsequent invocations spawn in parallel. Overlapping executions can quickly exhaust server memory, thread pools, and file descriptors.
Prevent task collisions by wrapping commands with the standard Linux flock advisory locking utility:
# The -n flag (non-blocking) exits immediately if the lockfile is currently held:
* * * * * flock -n /var/lock/worker_sync.lock /usr/bin/python3 /opt/app/sync.py >> /var/log/sync.log 2>&1 Frequently asked questions
Why are both 0 and 7 accepted for day of week in cron?
In POSIX and Vixie cron implementations, both 0 and 7 designate Sunday. This prevents syntax confusion between international and regional calendar conventions where Sunday is indexed either as the first day (0) or the final day (7) of the week.
How can I debug a crontab command without waiting for the schedule?
Execute the command manually with cron’s bare environment: env -i HOME="$HOME" PATH="/usr/bin:/bin" SHELL="/bin/sh" /path/to/script.sh. This accurately reproduces the cron execution sandbox and flags missing PATH binaries or environment variables immediately.
What is the difference between a user crontab and /etc/crontab?
User crontabs (accessed via crontab -e) define 5 time fields followed directly by the command. System crontabs (/etc/crontab and /etc/cron.d/*) require an explicit 6th field specifying the user account under which the command executes (e.g., * * * * * root /opt/task.sh).
What happens if a cron job takes longer than its interval?
Cron forks a new process regardless of whether the prior run has finished. Heavy jobs can snowball, exhausting system RAM and file descriptors. Protect your commands by wrapping them with flock: flock -n /var/lock/job.lock /path/to/job.sh.
Why does date +%Y-%m-%d fail inside crontab?
In crontab syntax, unescaped percent signs (%) are translated into newlines, with all subsequent text piped to the command via stdin. To pass a literal % to the date command, you must escape it with a backslash: date +\%Y-\%m-\%d.