Cron Expression Cheatsheet With Real Examples
The handful of cron patterns you actually need, the difference between 5-field and 6-field syntax, and the timezone gotcha that will burn you.
Cron has been around since 1975 and nobody has invented a better way to say "run this thing every weekday at 9am." The syntax is infamous, but in practice you only need about eight patterns and you'll recognize all of them.
Here's the cheatsheet I actually reach for, plus the two or three gotchas that burn everyone at least once.
The Field Layout
Standard Unix cron is five fields, 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 accepts:
- A specific number:
5 - A list:
1,15,30 - A range:
9-17 - A step:
*/15(every 15 units) - A star:
*(any)
The trick everyone learns once and forgets: day-of-month and day-of-week are OR'd together. 0 0 1 * 1 doesn't mean "first of the month AND Monday." It means "first of the month, OR any Monday." Which is almost never what you want.
5-Field vs 6-Field (Quartz / Spring)
If you're writing cron for Unix, Kubernetes, GitHub Actions, Vercel, or most cloud schedulers — it's 5 fields.
If you're writing cron for Java (Quartz, Spring's @Scheduled), AWS EventBridge, or some older enterprise systems — it's 6 fields with seconds prepended:
second minute hour day-of-month month day-of-week [year]
Copy-pasting a 5-field expression into a 6-field parser will silently shift everything. 0 9 * * 1 (every Monday at 9am, Unix) becomes "every Monday of the 9th second of every minute" (garbage) in a Quartz parser. Always check which dialect you're writing for.
AWS EventBridge is a special flavor: 6 fields, but day-of-month and day-of-week are mutually exclusive — one must be ? if the other is set. This exists because Quartz decided to fix the OR-bug mentioned above and then never told anyone.
The Patterns You Actually Need
Daily at midnight
The classic. Nightly batch jobs, log rotation, "run this at quiet time."
Every weekday at 9am
Stand-up reminders, weekday-only reports, the "start of business" trigger.
Every 15 minutes
Polling cron. Be kind to whatever you're polling.
Hourly except peak hours
Useful for heavy jobs you want to avoid running during the business day.
First of every month at 02:00
Billing runs, monthly reports, "reset counters."
Every Sunday at 3am
Every 30 seconds (Quartz only)
Classic Unix cron can't go sub-minute. If you need sub-minute, you need Quartz, systemd timers, or a custom loop.
The "Last Day of Month" Trick
There's no L in standard Unix cron. (Quartz has it: 0 0 L * ? means "midnight on the last day.") In Unix cron, the workaround is:
The cron fires every day from the 28th through the 31st at 23:55. The shell check passes only when tomorrow is the 1st — i.e. today is actually the last day. It's ugly, but it's the canonical solution and you'll see it in every devops wiki.
The Timezone Gotcha (This Is the One)
Cron's timezone is determined by the environment it runs in. Your server's TZ env var. And most cloud schedulers are UTC by default:
- Vercel Cron → UTC
- GitHub Actions → UTC
- AWS EventBridge → UTC (by default; you can set a timezone on newer rules)
- Kubernetes CronJobs → historically UTC, now accepts
spec.timeZone
Your local laptop crontab, though? Whatever your system clock is.
This is the failure mode: you tested 0 9 * * 1-5 locally in Jakarta (WIB, UTC+7) and it fired at 9am. You deployed to Vercel and now it fires at 4pm your time, because UTC 09:00 is WIB 16:00.
Before you ship a cron expression to any cloud service, check whether it runs in UTC. If it does, convert your intended local time to UTC. "9am Jakarta weekdays" on Vercel is 0 2 * * 1-5, not 0 9 * * 1-5. The cron-explainer shows the next runs in your local time so you can sanity-check.
Debugging When It's Not Firing
When a cron doesn't fire and you're sure the expression is right:
- Check the scheduler's timezone (see above)
- Check DST transitions — on the "spring forward" day, 2am–3am doesn't exist and any cron in that window silently skips. On "fall back," 1am–2am runs twice.
- Check that
day-of-monthandday-of-weekaren't fighting each other - Check for
@rebootor@dailyaliases your scheduler may not support - Check the cron daemon / runner logs — "the expression never fired" and "the expression fired but the command exited 127" look identical from outside
The Minimum Set to Memorize
0 0 * * *— midnight daily0 9 * * 1-5— weekdays at 9am*/15 * * * *— every 15 minutes0 2 1 * *— 2am on the 1st0 3 * * 0— 3am Sunday
Everything else is a variation of those five. Build up from the patterns you recognize instead of hand-crafting the bit-fields every time.
The Takeaway
Cron looks harder than it is. The syntax is stable. The patterns you need in real work are small. The thing that will actually get you is the timezone — every time you deploy to a new scheduler, ask "is this UTC?" before you ask "is my expression right?"
Try these tools
More articles
Favicon Sizes That Actually Matter in 2026
The classic 'generate 47 favicon files' advice is outdated. Here's the minimum viable set modern browsers and PWAs actually use.
CSV ↔ JSON Conversion Pitfalls: Escaping, Quoting, Encoding
CSV looks trivial until you ship it. Embedded commas, Excel's auto-formatting, UTF-8 BOMs, and the locale wars — with rules for getting it right.
Encrypt Text in Your Browser Without Sending It Anywhere
How WebCrypto's AES-GCM works, why PBKDF2 iteration count matters, and what passphrase-based encryption actually protects you from.