Dates & Time
Cron Syntax — A Complete Practical Guide
Cron expressions explained: 5 fields, operators, aliases, the day-of-month vs day-of-week OR-logic trap, and the patterns that cover 95% of real jobs.
Cron has been the default scheduler on Unix since 1975. The syntax is minimal, widely misremembered, and has exactly one surprise that catches engineers every year. This is the reference that fits in your head.
The five fields
A cron expression is five fields separated by whitespace. Some systems (Quartz, Jenkins, AWS EventBridge) add a seconds field at the start or a year field at the end — this guide covers the classic five-field POSIX syntax that runs on /etc/crontab, Kubernetes CronJob, GitHub Actions schedules, and most hosted schedulers.
┌─────────── minute (0-59)
│ ┌───────── hour (0-23)
│ │ ┌─────── day of month (1-31)
│ │ │ ┌───── month (1-12 or JAN-DEC)
│ │ │ │ ┌─── day of week (0-6 or SUN-SAT; 7 == 0 on many systems)
│ │ │ │ │
* * * * * command
0 3 * * * runs at 03:00 every day. */15 * * * * runs every 15 minutes. 0 0 1 * * runs at midnight on the first of every month.
The four operators
Every cron field understands four operators:
| Operator | Meaning | Example | Matches |
|---|---|---|---|
* | Every value | * * * * * | Every minute |
, | List | 0 9,12,17 * * * | 09:00, 12:00, 17:00 |
- | Range | 0 9-17 * * 1-5 | Hourly, 9-17, Mon-Fri |
/ | Step | */10 * * * * | Every 10 minutes |
Steps combine with ranges: 0 9-17/2 * * * runs at 9, 11, 13, 15, 17. Steps alone against * are shorthand for “start at the minimum and step”: */15 in the minute field is 0,15,30,45.
The aliases
A short list of convenience aliases is defined in Vixie cron and inherited by most modern schedulers:
@yearly (or @annually) → 0 0 1 1 *
@monthly → 0 0 1 * *
@weekly → 0 0 * * 0
@daily (or @midnight) → 0 0 * * *
@hourly → 0 * * * *
@reboot → once at startup
@reboot is not portable. Kubernetes CronJob, AWS EventBridge, and most cloud schedulers do not support it. Use it only on traditional cron daemons (/etc/crontab, crontab -e).
The OR-logic trap
Here is the one thing that trips up experienced engineers. When you specify BOTH day-of-month (field 3) and day-of-week (field 5), cron runs when either matches, not both.
# You might read this as "first of the month, but only if it is a Monday"
0 9 1 * 1
# Cron actually reads: "at 09:00 on the 1st of every month OR every Monday"
This is in POSIX, inherited by Vixie cron, inherited by the Linux and macOS default. Kubernetes CronJob, GitHub Actions, and almost every hosted cloud scheduler preserve the same behavior because they aim for compatibility.
The workaround is to use * in one of the two fields whenever you do not need both, and to test with a cron parser. If you truly need “first Monday of the month” you have to either script the check in the job itself (exit early if date +%u is not 1) or use a scheduler that supports Quartz-style # operators (1#1 = first Monday).
Patterns that cover 95% of real jobs
Build these into muscle memory:
# Every minute
* * * * *
# Every 5 minutes
*/5 * * * *
# Hourly at the top of the hour
0 * * * *
# Daily at 03:00
0 3 * * *
# Weekly Sunday at 04:00
0 4 * * 0
# Monthly on the 1st at midnight
0 0 1 * *
# Every weekday (Mon-Fri) at 09:00
0 9 * * 1-5
# Twice a day (08:00 and 20:00)
0 8,20 * * *
# Every 15 minutes during office hours Mon-Fri
*/15 9-18 * * 1-5
# Quarterly (Jan, Apr, Jul, Oct) on the 1st at 00:05
5 0 1 1,4,7,10 *
For the surrounding shell environment you will usually pair with cron, the bash cheatsheet covers redirections and the linux-cheatsheet has the daemon control commands.
The environment is not your shell
The single most common cron bug is: “it works when I run it manually, but it does not work as a cron job”. The job runs in a stripped-down environment. PATH is minimal. HOME may be unset on some systems. No shell profile is sourced.
Always:
- Use absolute paths (
/usr/bin/python3, notpython3). - Redirect output (
>> /var/log/myjob.log 2>&1) so you can see what cron silently swallowed. - Set a
PATH=at the top of the crontab if your job runs commands that need non-default paths. - Quote carefully. Percent signs
%have special meaning (newlines in the data sent to stdin); escape them as\%in the crontab.
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
MAILTO=""
# Daily backup at 02:30
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Timezones
On traditional Linux, cron runs in the server’s system timezone (whatever /etc/localtime says). On Kubernetes CronJob v1.27+, you can set .spec.timeZone: "Europe/Madrid" per job. AWS EventBridge supports cron() expressions in UTC only — convert yourself.
If your scheduler is in one zone and your team in another, the safest default is UTC for everything and mental math at the edges. See the timezone handling guide for the general principle: store and schedule in UTC, render local.
Testing without waiting
Three approaches, fastest to most accurate:
crontab.guruor any local cron parser. Paste the expression, see the next 5 run times in plain English.- Temporarily change the schedule to
* * * * *(every minute) and watch the log. - Run the command by hand with the cron environment:
env -i /bin/bash -c 'YOUR COMMAND'reproduces cron’s empty environment.
Takeaways
Five fields, four operators, one trap (day-of-month OR day-of-week). Pair the job with absolute paths, explicit redirects, and a timezone decision that matches your whole system. For the broader Linux operational reference, see the linux cheatsheet; for shell patterns the jobs themselves usually call into, see the bash cheatsheet.