How to Read and Write a Crontab Cron Expression Without Guessing
Learn the five cron fields, common scheduling patterns, the @daily shortcuts, and the timezone and overlap pitfalls that quietly break scheduled jobs.
How to Read and Write a Crontab Cron Expression Without Guessing
A cron expression is five small numbers in a row, and every one of them controls when a job fires. The format looks innocent, but I have watched a one-character mistake turn a "run every weekday" backup into a "run only on Mondays" backup that nobody noticed for three weeks. Once you understand what each field means and where the syntax bites, reading and writing these lines stops being a memory test.
This is a practical walk through the five fields, the patterns you will actually type, the @daily-style shortcuts, and the two failure modes that catch people most often: server timezone and overlapping jobs.
The Five Fields, Left to Right
A classic crontab line has exactly five time fields followed by the command:
* * * * * /path/to/command
│ │ │ │ │
│ │ │ │ └── day of week (0–6, Sunday is 0)
│ │ │ └──── month (1–12)
│ │ └────── day of month (1–31)
│ └──────── hour (0–23)
└────────── minute (0–59)
So the order, left to right, is minute, hour, day-of-month, month, day-of-week. That order never changes. If you can recite those five words, you can read any cron line by walking across it.
Inside each field you have four building blocks:
*means every valid value for that field. A*in the minute field means every minute; a*in the month field means every month.- A specific number pins the field.
0in the minute field means "at minute zero". - A list with commas means "these exact values":
1,15,30in the minute field fires three times an hour. - A range with a hyphen means "from here to there":
1-5in the day-of-week field is Monday through Friday.
Then there is the step operator, /. Writing */5 in the minute field means every 5 minutes — it counts 0, 5, 10, 15, and so on. One detail worth burning into memory: a step counts from the start of the field's range and does not roll across the boundary. In the hour field, */5 gives 0, 5, 10, 15, 20, then resets at midnight, so the gap from 20:00 to the next run at 00:00 is four hours, not five. Cron does not carry the remainder into the next day.
A Worked Example: 0 9 * * 1-5
Take the line 0 9 * * 1-5 and read it field by field.
- Minute:
0— at minute zero. - Hour:
9— at 9 o'clock. - Day of month:
*— every day of the month. - Month:
*— every month. - Day of week:
1-5— Monday through Friday.
Put it together and it says: at 9:00 AM, Monday through Friday. That is the canonical "start of the business day on weekdays" schedule. Change the hour to 0 18 * * 1-5 and you have an end-of-day report; change the day-of-week to 1-5 plus a step in the minute field, like */15 9-17 * * 1-5, and you get a job every fifteen minutes during working hours on weekdays.
If you want to confirm a line like that before it ships, the Crontab Helper parses an expression straight back into clickable panels, prints a plain-English sentence, and previews the next five firing times. Pasting in an inherited line and watching the panels light up is the fastest way to learn what a strange expression actually does.
Common Patterns You Will Type Constantly
A handful of shapes cover most real jobs:
- Every 5 minutes:
*/5 * * * * - Every hour, on the hour:
0 * * * * - Every day at 3 AM:
0 3 * * * - Every weekday at 9 AM:
0 9 * * 1-5 - Every Sunday at midnight:
0 0 * * 0 - First of the month at 6 AM:
0 6 1 * *
Notice the pattern: to make a job run less often, you pin more fields to specific values. * * * * * is the busiest possible line (every minute), and each number you add narrows it down.
The @ Shortcuts
Most cron implementations accept named shortcuts in place of the five fields:
@hourlyis the same as0 * * * *@daily(also@midnight) is0 0 * * *@weeklyis0 0 * * 0@monthlyis0 0 1 * *@yearly(also@annually) is0 0 1 1 *
There is also @reboot, which runs once when cron starts after a boot rather than on a clock. The shortcuts are readable, but they hide the exact time, and not every scheduler supports them, so I treat them as convenient sugar rather than something to rely on in portable scripts.
Pitfall One: The Server's Timezone
This is the one that has burned me personally. I once scheduled 0 6 1 * * for a monthly invoice run, tested it, and moved on. The job fired correctly — at 6 AM in the server's timezone, which was UTC. I am in UTC+8, so to me it ran at 2 PM, eight hours off from what finance expected. Nothing was broken; the expression was exactly what I asked for. The lesson stuck: a cron schedule runs on the server's clock, not yours.
Before you trust any schedule that has a human-facing deadline, check what timezone the host actually uses (timedatectl or date on Linux). If there is a gap, either shift the hour in the expression to compensate or run the job under an explicit TZ= variable so the line documents its own intent. When you are juggling offsets across regions, the Timezone Converter is faster than doing the arithmetic in your head, and it removes the off-by-one-day errors that creep in near midnight.
Pitfall Two: Overlapping Jobs and the Top-of-Hour Stampede
The second trap shows up under load. Three teams each write 0 * * * * for their hourly job, and every one of them fires at minute zero. Now the top of every hour is a thundering herd, CPU spikes, and the jobs that were quick in isolation start tripping over each other. The fix is simple once you see it: spread the minute offsets. Give one job 7 * * * *, another 23 * * * *, a third 41 * * * *. Same cadence, no collision.
A related version of the problem is a single slow job that runs more often than it finishes. If a five-minute task is scheduled */5 * * * * but sometimes takes seven minutes, cron will happily start a second copy on top of the first. Cron does not check whether the previous run is still going. Guard against it in the job itself with a lockfile (flock is the standard tool on Linux) so a long run blocks the next start instead of stacking.
A Few Syntax Traps Worth Naming
0 2 * * 1,5is Monday and Friday only, not "every weekday". The comma lists specific days; the hyphen1-5is the weekday range.0 0 31 * *silently skips months that have no 31st. Plain cron has no "last day of month" token, so months with 28, 29, or 30 days just pass it by.- Setting both day-of-month and day-of-week (for example
0 0 13 * 5, aiming for "Friday the 13th") backfires. The common Vixie cron treats the two fields as OR, so it fires every 13th and every Friday. Leave one of them as*unless you genuinely want the union.
Cron is a small language, but it is precise and unforgiving — it does exactly what the five fields say, never what you meant. Read each line field by field, preview the next runs, and check the timezone before you trust it, and these expressions become routine instead of a source of 3 AM surprises.
Made by Toolora · Updated 2026-06-13