DevOps

How to Read a Cron Expression (Without Googling It Every Time)

Cron syntax is five fields, each with its own rules. Here's how to actually read one, plus the gotchas that cause scheduled jobs to run at the wrong time.

Nikhil Sai··4 min read
All posts

A cron expression is five space-separated fields — minute, hour, day-of-month, month, day-of-week — each specifying when a job runs. 0 9 * * 1-5 means "9:00 AM, Monday through Friday." The hard part isn't the syntax, it's the gotchas in day-of-month/day-of-week interaction and timezone handling that cause jobs to silently run at the wrong time.

The five fields

┌───────────── 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 value, a range, a list, a step, or a wildcard:

  • * — every value
  • 5 — exactly that value
  • 1-5 — a range (inclusive)
  • 1,3,5 — a list
  • */15 — every N units (step)

Common patterns worth memorizing

| Expression | Meaning | |---|---| | * * * * * | Every minute | | 0 * * * * | Every hour, on the hour | | 0 9 * * * | Every day at 9:00 AM | | 0 9 * * 1-5 | Weekdays at 9:00 AM | | */15 * * * * | Every 15 minutes | | 0 0 1 * * | Midnight on the 1st of every month | | 0 0 * * 0 | Every Sunday at midnight | | 0 */6 * * * | Every 6 hours (00:00, 06:00, 12:00, 18:00) |

The gotcha almost everyone hits: day-of-month AND day-of-week

If you specify both day-of-month and day-of-week as non-wildcard values, most cron implementations treat them as OR, not AND. 0 0 15 * 5 doesn't mean "the 15th, if it's a Friday" — it means "midnight on the 15th, OR every Friday at midnight." This trips people up constantly when they want a schedule like "the first Friday of the month," which cron's five-field syntax genuinely can't express on its own — you need application logic on top, or a scheduler with extended syntax (L, #).

The other gotcha: whose timezone

Cron doesn't carry timezone information — it runs in whatever timezone the executing system (or cron daemon) is configured for. This is the single most common cause of "the job ran three hours late" bugs:

  • A cron job on a server set to UTC running 0 9 * * * fires at 9:00 AM UTC, not 9:00 AM in your local time
  • Some platforms (Vercel Cron, GitHub Actions, most managed schedulers) explicitly run in UTC regardless of your system's local time
  • Daylight saving transitions can cause a 0 2 * * * job to run twice, or not at all, on systems that use local time with DST

If a schedule needs to be "9 AM in New York" year-round, either configure the scheduler explicitly for that timezone if it supports it, or calculate the UTC offset yourself — and remember it changes twice a year with DST.

Day-of-week numbering isn't always consistent

Standard cron uses 0-6 for Sunday through Saturday, with 7 also accepted as Sunday in some implementations (Vixie cron, systemd timers). Not every scheduler agrees on this, so if a job seems to run on the wrong day, check whether the platform you're on treats 0 and 7 the same way — most do, but it's worth confirming rather than assuming.

Extended syntax (not standard, but common)

Many modern schedulers (Quartz, some Kubernetes CronJob-adjacent tools) support extensions beyond the POSIX five fields:

  • L — last day of the month/week (L in day-of-month = last day of the month)
  • # — nth weekday of the month (5#1 = first Friday)
  • ? — "no specific value," used to avoid the day-of-month/day-of-week OR ambiguity in some implementations

These aren't portable across every cron implementation, so check your specific scheduler's docs before relying on them.

Debugging a schedule you didn't write

The fastest way to understand an unfamiliar cron expression — inherited from a teammate, found in a Kubernetes CronJob spec, buried in a CI config — is to decode it rather than mentally parse five cryptic fields. A Cron Expression Parser translates the expression into plain English and previews the next several run times, which also makes it easy to catch the day-of-month/day-of-week OR trap before it causes a production surprise.

Quick checklist

  • Confirm which timezone your scheduler actually runs in — don't assume local time
  • If you specify both day-of-month and day-of-week, remember it's OR, not AND
  • Test with a preview/next-run-times tool before deploying a new schedule
  • For "nth weekday of month" logic, check if your scheduler supports #, or handle it in application code

Related posts