Reading and writing cron expressions
Field order, the difference between */5 and 5, the day-of-month and day-of-week trap, and how to sanity-check a schedule.
Last reviewed 8 September 2026
Cron syntax is five numbers and some punctuation, and it is responsible for a remarkable number of incidents: the backup that ran every minute, the report that arrived at 3 a.m. in the wrong time zone, the cleanup job that never fired at all. The syntax is small enough to learn properly in one sitting.
The five fields
┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌─────── month (1–12)
│ │ │ │ ┌───── day of week (0–7, where 0 and 7 are both Sunday)
│ │ │ │ │
* * * * *Order matters and there is no forgiveness for getting it wrong: a transposition produces a valid expression with a completely different meaning.
The operators
*— every value for this field,— a list:1,15,30-— a range:9-17/— a step:*/15means every fifteenth value
The mistake almost everyone makes once
*/5 and 5 look similar and are entirely different.
| Expression | Meaning | Runs per day |
|---|---|---|
*/5 * * * * | Every 5 minutes | 288 |
5 * * * * | At minute 5 of every hour | 24 |
5 5 * * * | At 05:05 once a day | 1 |
The reverse error is the expensive one. Intending an hourly job and writing * * * * * gives you one run per minute, which on a job that sends email or calls a paid API becomes noticeable quickly.
The day-of-month and day-of-week trap
This is the genuinely counter-intuitive rule. When both the day-of-month field and the day-of-week field are restricted, standard cron runs the job when either matches, not when both do.
0 0 1 * 1 → midnight on the 1st of the month, AND every MondayIf you wanted “the first Monday of the month”, this is not it, and it will fire roughly five times as often as intended. Achieving that properly requires either a scheduler supporting Quartz-style syntax (where # means the nth weekday) or a guard inside the job itself. Leave one of the two fields as * whenever you can.
Ranges include their endpoint
9-17 in the hour field covers hours 9 through 17 inclusive, and hour 17 means the whole of the 17th hour. So */15 9-17 * * 1-5 has its last run at 17:45, not at 17:00. Whether that is what you wanted is worth checking before it matters.
Time zones
A cron expression contains no time zone. It is interpreted in whatever zone the scheduler runs in, which is frequently UTC on a server and local time on a laptop. Two consequences follow:
- A job set for “09:00” may run at 09:00 UTC, which is a different hour for most of the world.
- In a zone observing daylight saving, a job scheduled inside the transition window can be skipped or run twice, once a year, which produces bugs that appear and disappear on a six-month cycle.
Scheduling anything sensitive at 02:30 local time is asking for it. Use UTC for infrastructure jobs, and make jobs idempotent so that running twice is harmless.
Common expressions worth recognising
| Expression | Meaning |
|---|---|
0 * * * * | Hourly, on the hour |
0 3 * * * | Daily at 03:00 |
0 3 * * 0 | Weekly, Sunday at 03:00 |
0 3 1 * * | Monthly, the 1st at 03:00 |
*/10 * * * * | Every ten minutes |
0 9-17 * * 1-5 | Hourly during working hours, weekdays |
Dialects differ
Standard Unix cron uses five fields. Quartz, used by many Java schedulers, uses six or seven and adds seconds, a year field, and tokens such as L for last, W for nearest weekday and # for the nth weekday. Some cloud schedulers accept shorthands such as @daily. An expression that is valid in one system may be rejected or interpreted differently in another, so check your scheduler’s own documentation before trusting a pattern from the internet.
Check before you commit
Paste the expression into the cron expression parser and read the English description back. If it does not say what you meant, change one field at a time and watch the sentence change — that is the fastest way to identify which field is wrong. Then confirm the scheduler's time zone separately, because no parser can tell you that.