Field Reference
``
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12 or jan-dec)
│ │ │ │ ┌───── day of week (0-6 or sun-sat, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *
`
| Syntax | Meaning | Example |
|---|---|---|
* | Every value | * * * * * = every minute |
5 | That value | 5 * * * * = 5 past every hour |
1-5 | Range | 0 9 * * 1-5 = 9am on weekdays |
1,15 | List | 0 0 1,15 * * = 1st and 15th |
*/15 | Step | */15 * * * * = every 15 minutes |
5/10 | From 5, then step | 5/10 * * * * = 5, 15, 25, 35, 45, 55 |
The Day-Field Trap
This is the one that catches everyone:
| Expression | What people expect | What cron does |
|---|---|---|
0 0 13 * 5 | Friday the 13th | Every 13th and every Friday |
0 0 1 * 1 | The 1st, if it is a Monday | Every 1st and every Monday |
0 0 * * 1 | Every Monday | Every Monday ✓ |
0 0 1 * * | Every 1st | Every 1st ✓ |
When *both* day fields are restricted, cron takes the union. To get an intersection you
need a guard in the job itself:`bash
0 0 13 * * [ "$(date +\%u)" = "5" ] && /run/job.sh
`
Shorthands
| Alias | Equivalent |
|---|---|
@yearly / @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily / @midnight | 0 0 * * * |
@hourly | 0 * * * * |
Time Zones Are the Real Bug
| Platform | Evaluates cron in |
|---|---|
| Unix crontab | System local time |
| Vercel Cron | UTC |
| GitHub Actions | UTC |
| Kubernetes CronJob | UTC unless spec.timeZone is set |
| AWS EventBridge | UTC unless specified |
If a schedule crosses midnight when converted, the day-of-week field shifts too. Weekdays
at 5pm in UTC-07:00 is 0 0 * * 2-6, not 0 0 * * 1-5.Scheduling Sensibly
Avoid0 * * * *for anything that hits a shared API — everyone's hourly job fires
at the same second. Use a random minute.
- Make jobs idempotent. Cron will double-fire eventually: DST, a restart, a retry.
- Never rely on the previous run finishing. Overlapping runs are the default; add a
lock file or a distributed lock.Five Fields, Read Left to Right
`
┌───────── minute (0–59)
│ ┌─────── hour (0–23)
│ │ ┌───── day of month (1–31)
│ │ │ ┌─── month (1–12)
│ │ │ │ ┌─ day of week (0–6, Sunday = 0)
│ │ │ │ │
* * * * *
`
Each field takes a value, a list (1,15), a range (9-17), a step (*/5) or * for
every value. Fields combine as an intersection — 0 9-17 * * 1-5 means hours 9 to 17 and
Monday to Friday — with one important exception below.
The Traps, in Order of How Often They Bite
1. The two day fields take a union, not an intersection. This is the exception, and it
surprises everyone. When both day-of-month and day-of-week are restricted, cron matches
*either*:
`
0 0 13 * 5 # every 13th AND every Friday — not Friday the 13th
`
Leave one as * unless you genuinely want both sets.
2. Steps are absolute, not relative. */10 matches minutes 0, 10, 20, 30, 40, 50 — not
"ten minutes after the last run finished". If a job starting at 10:00 takes nine minutes, the
next still starts at 10:10.
3. Steps that do not divide the range leave a gap. */7 in the minute field fires at 0,
7, … 56 and then waits four minutes. The clean hour divisors are 1, 2, 3, 4, 6, 8 and 12; for
minutes, 1, 2, 3, 4, 5, 6, 10, 12, 15, 20 and 30.
4. The hour field is 0–23. Noon is 12, midnight is 0, and there is no am or pm.
12 0 * * * is twelve minutes past midnight, which is not what anyone means by it.
5. There is no L in POSIX cron. "Last day of the month" needs a guard in the job.
Quartz and AWS EventBridge support L; crontab, Kubernetes, GitHub Actions and Vercel do
not.
Time Zones Decide More Than People Expect
Standard crontab evaluates in system local time. **Vercel Cron, GitHub Actions, Kubernetes
CronJobs and AWS EventBridge all evaluate in UTC.**
Converting a local time to UTC can move the day, and then the day-of-week field must move
too. Weekdays at 5pm in UTC−07:00 is 0 0 * * 2-6 — Tuesday to Saturday — not
0 0 * * 1-5. Deploying the unconverted expression gives you a job that runs on the right
clock time and the wrong days.
Local-time schedules also break twice a year: a job at 02:30 does not run at all on the
spring-forward date, and may run twice on the fall-back date. Scheduling in UTC avoids both,
at the cost of drifting an hour against local working hours.
Write Jobs That Survive Their Own Schedule
Cron guarantees far less than people assume, so the job has to make up the difference:
Assume it can run twice. Restarts, retries and DST all cause it. Make the work
idempotent rather than hoping.
- Assume runs overlap. Cron starts the next one whether or not the last finished. Take a
lock: flock -n /tmp/job.lock exits rather than queueing, which is what you want.
Avoid:00`. Every hourly job on the internet fires at the top of the hour. A random
- Alert on missing success, not on failure. A job that never starts produces no failure
Finding the Expression You Want
The pages here are indexed by what you would say out loud rather than by syntax — [every 5 minutes](/dev/cron-expression-generator/cron-every-5-minutes), [every hour](/dev/cron-expression-generator/cron-every-hour), [weekdays at 9am](/dev/cron-expression-generator/cron-every-weekday), [business hours](/dev/cron-expression-generator/cron-business-hours), [first of the month](/dev/cron-expression-generator/cron-first-day-of-month), [last day of the month](/dev/cron-expression-generator/cron-last-day-of-month), [quarterly](/dev/cron-expression-generator/cron-every-quarter). Each parses its own expression field by field and previews the next runs, so you can check the schedule before deploying it rather than by waiting a day to find out.