Cron schedule examples
58 ready-made cron expressions, each with a field breakdown, the next scheduled runs and equivalents for GitHub Actions, Kubernetes and AWS EventBridge. Need something else? Build a custom expression.
The field layout
┌───────── minute (0 - 59)
│ ┌─────── hour (0 - 23)
│ │ ┌───── day of month (1 - 31)
│ │ │ ┌─── month (1 - 12)
│ │ │ │ ┌─ day of week (0 - 6, Sunday = 0)
│ │ │ │ │
* * * * *
Shorthand aliases
@yearly / @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily / @midnight | 0 0 * * * |
@hourly | 0 * * * * |
@reboot | once at startup |
Day-of-month and day-of-week are OR, not AND
This is the one that catches everybody. When both the day-of-month and the day-of-week fields are restricted — neither is * — cron runs the job when either matches, not when both do.
0 0 1 * 1 # not "the 1st, if it is a Monday"
# it is "the 1st" OR "every Monday"
So that expression fires on the first of every month and on all four or five Mondays as well — roughly six times a month rather than the once you were picturing. It is specified behaviour, not a bug: POSIX says so and Vixie cron, the one behind most Linux systems, implements it.
When only one of the two is restricted the surprise disappears, which is why it stays hidden until the day you need "the first Monday of the month" — an expression standard cron cannot write at all. The usual answer is 0 0 1-7 * 1, which fires on every Monday and on the 1st to 7th, and then a guard inside the job checks the date.
What the step syntax actually counts from
*/5 in the minute field means minutes 0, 5, 10 and so on — it steps through the field's own range, not from whenever the job was installed. A job set to */5 at 09:03 first runs at 09:05, not 09:08.
Steps also apply to a range: 0-30/10 is minutes 0, 10, 20 and 30. And a step that does not divide the range evenly simply stops early rather than wrapping — */7 in the hour field gives 0, 7, 14 and 21, then a five-hour gap over midnight.
Cron has no timezone of its own
Jobs run in the system's local time, which means a schedule crossing a daylight-saving boundary either runs twice or not at all. Vixie cron treats jobs in the skipped hour as due immediately when the clock jumps forward, and jobs in the repeated hour as already run; other implementations differ, and container schedulers usually run UTC regardless of the host.
The reliable fix is to set the timezone explicitly where the scheduler allows it — CRON_TZ=UTC at the top of a crontab, timeZone in a Kubernetes CronJob, or the schedule expression's own timezone field in EventBridge — and to keep anything that must not run twice out of 01:00–03:00 entirely.
Five fields, unless it is six
Standard Unix cron takes five fields, starting at minutes. Quartz and Spring's scheduler take six, starting at seconds, so an expression copied between them is shifted by one place and silently means something else. 0 0 12 * * ? is noon in Quartz. Paste it into a crontab and Unix cron reads the first five fields as the schedule and everything after them as the command — so it becomes a job that runs at 00:00 on the 12th of every month and tries to execute a program called ?. It is accepted, it is scheduled, and it fails somewhere you are not looking, which is worse than a syntax error would have been.
Quartz also adds characters standard cron does not have — L for last, W for nearest weekday, # for "the nth weekday of the month". 0 0 12 ? * 2#1 is the first Monday of the month, which is the expression standard cron cannot express. If a snippet contains any of those, it is not a Unix crontab line.