ZairussalamTools

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.

·Ibrahimsyah Zairussalam·

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]
Don't paste between the two

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

Every day at 00:00

The classic. Nightly batch jobs, log rotation, "run this at quiet time."

Every weekday at 9am

Monday through Friday at 09:00

Stand-up reminders, weekday-only reports, the "start of business" trigger.

Every 15 minutes

Every 15 minutes, on the quarter

Polling cron. Be kind to whatever you're polling.

Hourly except peak hours

Every hour, skipping 9am–5pm

Useful for heavy jobs you want to avoid running during the business day.

First of every month at 02:00

Monthly billing job

Billing runs, monthly reports, "reset counters."

Every Sunday at 3am

Weekly maintenance window

Every 30 seconds (Quartz only)

Quartz 6-field syntax

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:

Fake 'last day of month' in Unix cron

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.

Always convert to UTC before deploying

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:

  1. Check the scheduler's timezone (see above)
  2. 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.
  3. Check that day-of-month and day-of-week aren't fighting each other
  4. Check for @reboot or @daily aliases your scheduler may not support
  5. 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 daily
  • 0 9 * * 1-5 — weekdays at 9am
  • */15 * * * * — every 15 minutes
  • 0 2 1 * * — 2am on the 1st
  • 0 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?"