The five-field format
Standard Unix cron — the kind that runs on every Linux box and underpins most scheduling systems — uses a five-field expression: minute, hour, day-of-month, month, day-of-week. Each field accepts a number, a wildcard, a list, a range, or a step.
Visually: '* * * * *' reads as 'every minute of every hour of every day of every month of every day-of-week', which is to say, every minute. '0 9 * * *' reads as 'at minute 0 of hour 9, every day' — 9 a.m. daily. '*/15 * * * *' is 'every 15 minutes'. '0 0 1 * *' is 'midnight on the first of every month'.
Allowed ranges: minute 0–59, hour 0–23, day-of-month 1–31, month 1–12 (or JAN–DEC), day-of-week 0–6 with 0 being Sunday (or SUN–SAT).
The day-of-month vs day-of-week trap that bites everyone
Here is the single biggest gotcha in cron, and it has bitten every engineer who has ever written a non-trivial expression. When you set BOTH the day-of-month field and the day-of-week field to something other than '*', cron uses OR semantics, not AND.
Example: '0 9 * 1 1' looks like 'at 9 a.m., in January, on Mondays'. It is in fact 'at 9 a.m., every day in January, AND on every Monday of the year'. The job fires on every January day and on every Monday — far more often than a casual reading suggests.
If you want AND semantics — 'Mondays in January only' — standard cron cannot express it. You have to set one field to '*' and filter the other in your application code, or use a flavour like Quartz that has stricter semantics and a '?' wildcard.
The rule of thumb: if you set day-of-month AND day-of-week, you almost certainly have a bug. One of them should be '*'.
GitHub Actions cron
GitHub Actions uses POSIX cron syntax but with three important quirks. First, schedules are always UTC — there is no timezone setting. If you want a job to run at 9 a.m. local time in a place with daylight-saving, your cron expression will need to change twice a year.
Second, the minimum resolution is roughly 5 minutes. You can write '*/1 * * * *' but GitHub will skip executions to keep load manageable.
Third, there is no guarantee the job runs exactly on the minute. GitHub queues scheduled workflows and runs them as capacity permits; a few minutes of drift is normal. If your workflow is time-critical to the second, GitHub Actions is the wrong scheduler.
Kubernetes CronJob
Kubernetes CronJob takes a standard five-field expression in the .spec.schedule field. Since Kubernetes 1.27, you can also set .spec.timeZone to an IANA timezone name (e.g. 'America/Toronto') so the schedule fires at local time, with DST handled correctly. Before 1.27, everything was UTC.
Three settings deserve attention. .spec.startingDeadlineSeconds caps how late a missed run can still start. .spec.concurrencyPolicy controls what happens if the previous run is still going when the next one is supposed to start: Allow, Forbid, or Replace. .spec.successfulJobsHistoryLimit and .spec.failedJobsHistoryLimit decide how many old Job objects pile up.
AWS EventBridge (and the six-field rule)
AWS EventBridge — the modern replacement for CloudWatch Events — uses a SIX-field cron expression, not five. The fields are minute, hour, day-of-month, month, day-of-week, and year. This is the single most common reason a cron expression copied from Stack Overflow does not work in AWS.
EventBridge also requires that exactly one of day-of-month and day-of-week is '?' and the other is something concrete. A '*' in both is rejected.
EventBridge supports the Quartz extensions 'L', 'W', and '#'. Timezone is configurable per rule, and DST is honoured correctly. The 'rate(...)' syntax — 'rate(5 minutes)', 'rate(1 hour)' — is an alternative to cron for fixed-interval schedules and is often easier to read.
Twenty patterns you will actually use
- '* * * * *' — every minute
- '*/5 * * * *' — every 5 minutes
- '*/15 * * * *' — every 15 minutes
- '0 * * * *' or '@hourly' — top of every hour
- '0 */2 * * *' — every 2 hours on the hour
- '0 9 * * *' — 9 a.m. every day
- '30 6 * * *' — 6:30 a.m. every day
- '0 9 * * 1-5' — 9 a.m. every weekday
- '0 9 * * 1' — 9 a.m. every Monday
- '0 22 * * 5' — 10 p.m. every Friday
- '0 0 * * 0' or '@weekly' — midnight every Sunday
- '0 0 1 * *' or '@monthly' — midnight on the 1st of every month
- '0 0 15 * *' — midnight on the 15th (a common payroll day)
- '0 0 L * *' (Quartz/EventBridge) — midnight on the last day of the month
- '0 0 1 1 *' or '@yearly' — midnight on January 1st
- '0 9 1 */3 *' — 9 a.m. on the 1st of every third month (quarterly)
- '0 9 8-14 * 2' — 9 a.m. on the second Tuesday of the month (standard cron trick)
- '0 9 ? * 2#1' (Quartz) — 9 a.m. on the first Monday of the month
- '0 2 * * 0' — 2 a.m. every Sunday (classic weekly backup window)
- '@yearly', '@monthly', '@weekly', '@daily', '@hourly' — named shortcuts
Tools used in this guide
FAQ
- Why did my cron fire on a day I didn't expect?
- Almost always the day-of-month vs day-of-week trap. If you set BOTH fields to something other than '*', standard cron uses OR semantics — the job fires whenever EITHER field matches. To get AND semantics ('Mondays in January only'), use Quartz with a '?' in one field, or set one field to '*' in standard cron and filter in your application code.
- Why doesn't my GitHub Actions cron fire exactly on the minute?
- Because GitHub queues scheduled workflows and runs them as capacity permits. A few minutes of drift is normal, more during peak load. The minimum effective resolution is around 5 minutes. If you need second-level precision, run your own scheduler.
- Does Kubernetes CronJob handle daylight saving time?
- Since Kubernetes 1.27, yes, if you set .spec.timeZone to an IANA timezone name like 'America/Toronto'. Before 1.27, schedules were UTC only and DST was your problem.
- Why does my AWS EventBridge cron expression get rejected?
- EventBridge uses six fields (the sixth is year) instead of the standard five — pasting a five-field expression will be rejected. EventBridge also requires exactly one of day-of-month and day-of-week to be '?' and the other to be concrete; '* * * * ? *' fires every minute, '0 9 * * ? *' fires 9 a.m. daily.