Cron is the oldest, dumbest, and most reliable scheduler most of us will ever use. The syntax fits on one line, which is exactly why people get it wrong — a single misplaced asterisk can turn a nightly backup into a job that fires every minute. This guide walks through the five fields, the special characters, the schedules you'll actually write, and the timezone traps that quietly cause incidents.
The five fields
A classic cron expression is five space-separated fields, read left to right:
text┌───────────── 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, 0 = Sunday) │ │ │ │ │ * * * * * command to run
All five asterisks (
* * * * *@ScheduledThe field that surprises people most is day-of-week. It overlaps with day-of-month, and the two interact in a way that breaks intuition (more on that below).
Special characters
Four characters do almost all the work.
Asterisk text* — every value
**Slash text/ — steps
/A step runs at fixed intervals across a range.
*/50-30/10A common mistake:
*/90*/90Comma text, — lists
,Commas join specific values.
0 9,12,17 * * *0 0,12 * * *Hyphen text- — ranges
-A hyphen is an inclusive range.
0 9-17 * * *1-5You can mix all of these in one field:
0 9-17/2 * * 1-5Common schedules you'll actually write
Here are the expressions worth memorizing.
text*/5 * * * * Every 5 minutes 0 * * * * Every hour, on the hour 0 0 * * * Every day at midnight (nightly) 30 2 * * * Every day at 2:30 AM 0 9 * * 1-5 9:00 AM, weekdays only 0 0 * * 0 Every Sunday at midnight 0 0 1 * * Midnight on the 1st of every month 0 0 1 1 * Midnight on January 1st (yearly) 15 14 1 * * 2:15 PM on the 1st of every month 0 22 * * 1-5 10:00 PM on weekdays
A few notes on these:
- Nightly jobs cluster at midnight. If every cron job on a box fires at , they all compete for CPU and I/O at once. Stagger them —text
0 0 * * *,text5 0,text17 0— so they don't stampede.text40 0 - 2:30 AM is a popular maintenance window because traffic is low, but see the DST warning below before you trust early-morning times.
- Weekday business hours () is the canonical "send the daily report" schedule.text
0 9 * * 1-5
Many schedulers also accept named shortcuts:
@hourly@daily@midnight@weekly@monthly@yearly@reboot0 0 * * *The day-of-month / day-of-week trap
This one bites everyone eventually. When both the day-of-month and day-of-week fields are restricted (neither is
*So this expression:
text0 0 13 * 5
does not mean "midnight on Friday the 13th." It means "midnight on the 13th of the month OR any Friday" — which is most of the time. To get an actual AND, you generally need a logic check in the command itself, or a scheduler dialect (like Quartz) that handles it differently. If your expression restricts both day fields and the behavior looks wrong, this is almost always why.
The safe habit: leave one of the two day fields as
*Timezone gotchas
Timezones are where cron quietly causes production incidents.
Cron uses the machine's local time
Classic cron evaluates expressions against the system clock and its configured timezone, not UTC. The same
0 9 * * *Asia/KolkataAmerica/New_YorkThe defensive move: standardize servers on UTC and do the mental conversion once, or set the timezone explicitly per job where your cron supports it (many modern crons accept a
CRON_TZ=text# Some cron implementations support this: CRON_TZ=America/New_York 0 9 * * 1-5 /usr/local/bin/daily-report
Daylight Saving Time creates missing and doubled runs
If your job runs during the DST transition window, two things can happen:
- Spring forward: the clock jumps from 1:59 to 3:00. A job scheduled for lands in a time that never existed that day. Depending on the implementation, it may be skipped entirely.text
30 2 * * * - Fall back: the clock repeats the 1–2 AM hour. A 2:30 job can run twice.
This is exactly why so many teams schedule maintenance at 2:30 AM and then discover their backup either vanished or duplicated twice a year. The fix is to schedule jobs outside the 1–3 AM local window, or run on UTC where there is no DST shift at all. If a job must be idempotent (safe to run twice), make it so — don't rely on cron firing exactly once across a DST boundary.
How to verify next runs before you trust an expression
Never deploy a cron expression you haven't sanity-checked. Reading the next several fire times out loud catches the day-of-week OR-trap, the
*/90Read it back in plain English. Force yourself to translate each field. "Minute 0, hours 9 through 17, every day, every month, weekdays" should match your intent word for word. Our cron expression builder does this translation live and lists the upcoming run times as you edit, so you can confirm the schedule before it ever reaches a server. It runs entirely in your browser — nothing about your schedules is uploaded.
Dry-run with the system parser when you can. On systems with
systemdtextsystemd-analyze calendar "Mon..Fri 09:00"
That prints the next elapse time using the exact logic the scheduler will use — the most trustworthy verification, because it's the same code path that will run the job.
Test the command separately from the schedule. A correct expression running a broken command still fails silently. Run the command by hand first, with the same user and environment cron will use. Cron jobs run with a minimal
PATHLog and alert. Append output to a file and check it after the first scheduled run:
text0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
The
2>&1Quick reference
- Five fields: minute, hour, day-of-month, month, day-of-week.
- = every,text
*= step,text/= list,text,= range.text- - Steps don't wrap fields — minutes won't give you 90-minute intervals.text
*/90 - Restricting both day fields means OR, not AND.
- Cron uses local time; standardize on UTC and avoid the 1–3 AM DST window.
- Always read the next runs back before deploying — build and verify in a cron builder, then confirm the command runs standalone with cron's stripped-down environment.
Cron rewards precision and punishes guessing. Spend the extra minute to verify the next handful of fire times, and you'll avoid the 3 AM page that says your nightly job has been running every minute since Tuesday.