Cron Expressions Explained: Mastering the Five Fields
What each of the five crontab fields means, the patterns you actually use, and the timezone/DST traps.
A cron expression is the standard way to say “run this on a schedule” — the same five-field syntax powers Linux crontab, GitHub Actions, Kubernetes CronJobs, Jenkins, and countless SaaS schedulers. A single line like 30 2 * * 1-5means “every weekday at 02:30”, but until you know the field order it reads like line noise. This guide walks through the five fields, the four operators (* , - /), a copy-paste table of common patterns, and the two traps that cause real outages: the day-of-month/day-of-week OR rule and timezone/DST surprises.
Whatever you write, verify it before it ships. Paste the expression into the cron next-run calculatorand check that the list of upcoming fire times matches what you had in mind — the gap between “what I meant” and “what cron parsed” is where most scheduling bugs live.
The five fields: minute, hour, day, month, weekday
A standard crontab expression is five space-separated fields, read left to right:
| Position | Field | Allowed values | Notes |
|---|---|---|---|
| 1 | Minute | 0-59 | |
| 2 | Hour | 0-23 | 24-hour clock |
| 3 | Day of month | 1-31 | |
| 4 | Month | 1-12 | Names jan-dec accepted |
| 5 | Day of week | 0-7 | Both 0 and 7 mean Sunday; names sun-sat accepted |
Inside each field you can use four operators:
*— every value. A*in the minute field means “every minute”.,— a list.0,30fires at minute 0 and minute 30.-— a range.1-5covers 1 through 5 inclusive (Monday to Friday in the weekday field)./— a step.*/5means 0, 5, 10, … and combines with ranges, e.g.10-50/10.
One subtlety: */Nmeans “values divisible by N counting from the start of the range”, not “N minutes after the last run”. Put */7 in the minute field and it fires at 0, 7, 14, …, 56, then resets to 0 at the top of the next hour — so the 56→0 gap is only four minutes. If you need a true fixed interval, cron alone cannot express it; reach for a systemd timer instead.
Worked example: decoding 30 2 * * 1-5
Take one of the most common production schedules, 30 2 * * 1-5, and read it field by field:
- Minute =
30→ at minute 30. - Hour =
2→ only during hour 2. Combined: 02:30. - Day of month =
*→ any day of the month. - Month =
*→ any month. - Day of week =
1-5→ Monday (1) through Friday (5).
Put together: “every weekday at 02:30” — the classic slot for backups and batch jobs that should run off-peak but skip weekends. Always read in the fixed order minute → hour → day → month → weekday and any expression decodes the same way.
Patterns you will actually use
| Expression | Meaning |
|---|---|
*/5 * * * * | Every 5 minutes (:00, :05, :10, …) |
0 * * * * | Hourly, on the hour |
0 3 * * * | Daily at 03:00 |
0 9 * * 1-5 | Weekdays at 09:00 |
0 0 1 * * | Midnight on the 1st of every month |
0 */6 * * * | Every 6 hours (00:00, 06:00, 12:00, 18:00) |
30 2 * * 0 | Sundays at 02:30 |
Crontab also accepts shortcuts: @hourly (= 0 * * * *), @daily (= 0 0 * * *), @weekly, @monthly, and @yearly are plain aliases, while @rebootis special — it fires once right after the system boots rather than at a clock time. Handy for cache warm-up or restarting a daemon, but not every cron implementation supports it (some containerized environments don't), so check before relying on it.
The big gotcha: day-of-month and day-of-week combine with OR
Here is the rule that trips up nearly everyone. If both the day-of-month field and the day-of-week field are restricted (neither is *), the job fires when either one matches — OR, not AND. That behavior is written into the POSIX crontab specification.
So 0 0 13 * 5 looks like “midnight on Friday the 13th” but actually fires on every 13th of the month and every Friday— four or five times a month. Likewise, “first Monday of the month” is not 0 9 1-7 * 1 (that runs on days 1–7 and every Monday). The standard fix is to keep only the weekday in the expression and add a date guard at the top of the script, e.g. [ $(date +\%d) -le 7 ] || exit 0.
These combined expressions are exactly the ones your eyes cannot verify, so run anything that restricts both fields through the cron next-run calculator and inspect the upcoming fire times before deploying.
Timezones and DST: stay away from the 2 a.m. hour
Crontab evaluates expressions in the system (or daemon) timezone. On a UTC server, 0 3 * * * runs at 03:00 UTC — which is noon in Seoul and 11 p.m. in New York. The same line fires at different wall-clock times on differently configured servers. The sanest setup for a fleet is to keep every server on UTC and convert timestamps when reading logs — the log timestamp converter turns UTC log lines into your local time in one paste.
Daylight saving time adds a nastier wrinkle. On spring-forward night the clock jumps from 02:00 straight to 03:00, so 02:30 never exists and a 30 2 * * * job may simply not run that day. On fall-back night 02:30 happens twice, so the job may run twice (Vixie cron has compensation logic; many other implementations do not). Three defenses:
- Run servers in UTC — UTC has no DST, so the problem disappears.
- If you must schedule in a DST timezone, avoid the 01:00–03:59 window.
- Make jobs idempotent or use a lock file so a duplicate run is harmless.
Environment quirks: PATH and the % character
The number one cause of “works in my terminal, fails in cron” is the environment. Cron does not run a login shell, never reads your .bashrc, and ships a minimal PATH like /usr/bin:/bin — which is why docker, node, or awsdie with “command not found”. Fix it either by declaring PATH=/usr/local/bin:/usr/bin:/bin at the top of the crontab or by using absolute paths for every binary.
Second quirk: in a crontab command line, % is special. Everything after the first unescaped % is fed to the command as standard input, and each % becomes a newline — so an innocent date +%F silently breaks. Escape it as date +\%F. Finally, never throw output away: append >> /var/log/myjob.log 2>&1 so you can actually see why a job failed.
Frequently asked questions
What is the difference between 0 and 7 in the day-of-week field?
What happens if I set both day-of-month and day-of-week?
Why do some cron expressions have 6 or 7 fields?
Does */5 mean 'five minutes after the previous run finished'?
Why does my script work in the terminal but fail under cron?
Is a 02:30 cron job risky on a server with daylight saving time?
Tools to use with this guide
Related guides
- WordPress Security Basics: 10 Checks That Stop Most HacksThe common ways WordPress gets hacked, and the login, plugin, file-permission and header settings that stop them.