Cron Expressions Explained: A Field-by-Field Cheat Sheet

Β· By the CalculatorHive editorial team

Key takeaways
  • Standard cron has five fields: minute (0–59), hour (0–23), day of month (1–31), month (1–12), day of week (0–6, where 0 and 7 both mean Sunday).
  • The special characters are * (every value), , (list), - (range), and / (step). 0 9 * * 1-5 means 09:00 Monday to Friday.
  • If both day-of-month and day-of-week are restricted, most cron implementations run the job when either matches, not both β€” 0 0 13 * 5 fires on the 13th and every Friday.
  • Cron follows the system's local clock, so a fixed-time job can be skipped or repeated at daylight saving transitions. Run schedule-sensitive jobs in UTC.

Cron syntax is compact to the point of being cryptic, and the failure mode is silent: a wrong expression does not error, it just runs at the wrong time, or never. This is a field-by-field reference with a table of expressions you can copy, the day-of-month versus day-of-week trap that catches almost everyone once, and the timezone behaviour that turns a nightly job into a twice-run or never-run job twice a year.

What are the five fields and their ranges?

A classic crontab line is five whitespace-separated schedule fields followed by the command:

PositionFieldAllowed valuesNotes
1Minute0–59β€”
2Hour0–2324-hour clock; midnight is 0
3Day of month1–31Values above a month's length simply never match
4Month1–12JAN–DEC names accepted by most implementations
5Day of week0–70 and 7 are both Sunday; SUN–SAT names usually accepted

So 30 4 * * * is 04:30 every day, and 0 0 1 1 * is midnight on 1 January. Names are convenient but not universally supported, so numbers are the safer choice in code you will move between systems.

Most cron daemons also accept shorthand macros in place of all five fields: @yearly (or @annually), @monthly, @weekly, @daily (or @midnight), @hourly, and @reboot, which runs once when the daemon starts rather than on a schedule.

What do the special characters do?

  • * β€” every valid value for that field. In the minute field it means all 60 minutes.
  • , β€” a list. 0 6,12,18 * * * runs at 06:00, 12:00, and 18:00.
  • - β€” an inclusive range. 0 9-17 * * * runs on the hour from 09:00 through 17:00, which is nine times, not eight.
  • / β€” a step, applied to a range. */15 in the minute field is 0, 15, 30, 45. You can combine it with an explicit range: 0-30/10 is 0, 10, 20, 30.

The step operator carries a trap worth internalising: steps do not wrap. Each field restarts at its own lower bound, so */7 in the minute field gives 0, 7, 14, 21, 28, 35, 42, 49, 56 β€” and then the next fire is 0 of the following hour, four minutes later, not seven. Likewise 0 */5 * * * runs at hours 0, 5, 10, 15, 20 and then 0, a four-hour gap. Use a step that divides the field range evenly (5, 10, 15, 20, 30 for minutes; 1, 2, 3, 4, 6, 8, 12 for hours) or accept the irregular interval. And */45 fires only at 0 and 45, almost never what its author intended.

A table of common expressions

ExpressionMeaning
* * * * *Every minute
*/5 * * * *Every 5 minutes
0 * * * *Every hour, on the hour
0 */6 * * *At 00:00, 06:00, 12:00, 18:00
30 3 * * *03:30 every day
0 9 * * 1-509:00 Monday through Friday
*/15 9-17 * * 1-5Every 15 minutes, 09:00–17:45, weekdays
0 22 * * 022:00 every Sunday
0 6 1,15 * *06:00 on the 1st and 15th of each month
0 0 1 * *Midnight on the first of every month
0 0 1 1,4,7,10 *Midnight on the first day of each quarter
5 0 * 8 *00:05 every day in August

Notice the 09:00–17:45 row. 9-17 in the hour field includes hour 17, and */15 fires at :00, :15, :30, and :45 within it, so the last run of the day is 17:45 rather than 17:00. Off-by-one errors in cron are usually range-inclusivity errors. If you want to see the next several fire times for an expression rather than reasoning about it, the cron expression calculator parses a schedule and lists them.

The day-of-month versus day-of-week gotcha

This is the single most surprising behaviour in cron, and it is deliberate. When both field 3 (day of month) and field 5 (day of week) are restricted β€” that is, neither is * β€” Vixie cron and its descendants (cronie on most Linux distributions, and the BSD crons) match on either condition, not both.

Consequences:

  • 0 0 13 * 5 does not mean "midnight on Friday the 13th". It means midnight on the 13th of every month, plus midnight every Friday β€” roughly 64 runs a year instead of one or two.
  • 0 3 1 * 1 runs at 03:00 on the first of the month and every Monday.
  • When one of the two fields is *, normal AND behaviour applies. 0 3 * * 1 is simply every Monday, and 0 3 1 * * is simply the 1st.

The practical rule: never restrict both fields at once unless you want the union. For a true intersection such as Friday the 13th, schedule on the broader field and put the narrower test inside the job β€” run on the 13th and have the script exit unless the weekday matches. This OR rule is also not universal: some cloud and container schedulers implement AND semantics or reject ambiguous expressions, so an expression can behave differently in a managed environment than on a server.

Timezones, DST, and why scheduled jobs drift

Cron evaluates against the system's local wall clock. If that clock observes daylight saving time, two things happen every year:

  • Spring forward. An hour of local time does not exist. A job scheduled at 02:30 has no 02:30 to run at. Cronie and Vixie cron handle this by running fixed-time jobs from the skipped interval once, immediately after the clock jumps; other implementations skip them entirely.
  • Fall back. An hour of local time occurs twice. Cronie suppresses the duplicate for fixed-time jobs, but jobs with wildcard or step-based times simply follow the clock and genuinely execute twice in the repeated hour.

Neither behaviour is what you want for a billing run or anything that is not idempotent. The defences, in order of preference: set the server or container timezone to UTC, which has no DST; or declare the timezone explicitly where your scheduler supports it β€” cronie and Vixie cron honour a CRON_TZ line at the top of a crontab, systemd timers take a timezone in their OnCalendar expression, and Kubernetes CronJob has a timeZone field. Failing that, avoid scheduling anything between 00:30 and 03:30 local time, where transitions happen in most jurisdictions.

Make jobs idempotent regardless β€” a job that can run twice without harm is immune to this whole category of problem. When you are reconciling log lines against a schedule across timezones, the Unix timestamp converter turns epoch seconds into a readable date and back, which is usually the fastest way to establish what actually ran when.

Six-field variants: Quartz, Spring, and friends

Not every "cron expression" has five fields. The Java-derived schedulers add a seconds field at the front:

  • Quartz uses six mandatory fields (second, minute, hour, day-of-month, month, day-of-week) plus an optional seventh for the year.
  • Spring (and therefore Spring Boot's @Scheduled annotation) uses six fields, seconds first, with no year field.

So 0 0 12 * * ? is noon daily in Quartz, whereas the same string in a Unix crontab is a syntax error or, worse, silently misread. Two further Quartz differences bite hard:

  • Day-of-week is 1–7 with 1 = Sunday, not 0–6. A Unix expression for Monday (1) means Sunday in Quartz.
  • Exactly one of day-of-month and day-of-week must be ? β€” Quartz resolves the OR ambiguity by forbidding it. Quartz also adds L (last: L in day-of-month is the last day of the month, 6L in day-of-week is the last Friday), W (nearest weekday), and # (6#3 is the third Friday).

Before pasting an expression from a search result, confirm how many fields your scheduler expects and which day-of-week base it uses. Getting either wrong shifts the job by a day with no error message.

Common questions

Why did my cron job not run even though the schedule is correct?

In order of likelihood: cron runs with a minimal environment, so PATH is short and your shell profile is never sourced β€” use absolute paths for both the interpreter and the script. Second, percent signs in a crontab command are read as newlines and must be backslash-escaped. Third, the crontab file needs a trailing newline on some systems. Fourth, the job did run but wrote to stdout, which cron mails to the user; if mail is not configured, that output vanishes. Redirect output to a log file.

How do I run a job more often than once a minute?

Standard cron cannot: one minute is the finest resolution of the five-field format. The usual workarounds are a one-minute cron entry wrapping a loop that sleeps between iterations, a systemd timer with a sub-minute OnUnitActiveSec, or a long-running worker process with its own internal scheduler. If you need sub-minute reliability, a supervised daemon is a better fit than cron.

What happens if a job is still running when the next scheduled run arrives?

Cron starts a second copy. It does no overlap prevention at all, and a slow job on a short schedule can pile up until the machine runs out of memory or the database deadlocks. Wrap the command in a lock β€” flock on Linux is the standard tool β€” or have the script take a lock itself and exit quietly if it cannot.

How do I know a scheduled job is actually running?

Cron's silence is indistinguishable from success, so you need external evidence. The common pattern is a dead-man's switch: the job pings a monitoring endpoint on completion, and the monitor alerts if the ping does not arrive within an expected window. Track missed runs as availability, not as isolated incidents β€” the uptime SLA calculator converts a target percentage into the downtime minutes you can afford in a month or a year, which is a useful way to decide how much a skipped nightly job actually costs.

Check an expression before you deploy it. Paste a cron schedule to see it described in plain English and get the next run times.

Open the Cron Expression Calculator β†’