Practical guide 5 min read

Cron Expressions Explained: From Five Fields to Production Scheduling

A field-by-field guide to cron syntax, the timezone traps that cause silent job misfires, and how to debug schedules before they break.

Cron has run scheduled jobs on Unix systems since 1975, and its five-field syntax now powers Kubernetes CronJobs, GitHub Actions, Spring’s @Scheduled, and dozens of CI systems. It is also the source of a remarkable number of production incidents — most of them caused by three misunderstandings this article will fix.

The five fields, precisely

┌──────── minute        (0–59)
│ ┌────── hour          (0–23)
│ │ ┌──── day-of-month  (1–31)
│ │ │ ┌── month         (1–12)
│ │ │ │ ┌ day-of-week   (0–7, 0 and 7 both = Sunday)
* * * * *

Special syntax composes with any field:

  • * — every value
  • */5 — every 5th value (in minutes: :00, :05, :10…)
  • 1-5 — an inclusive range
  • 1,15,30 — a list
  • 1-10/2 — a range with a step

So 30 4 * * 1-5 means 04:30 on weekdays. 0 9 1 * * means 09:00 on the first of every month.

Misunderstanding #1: your job runs in the server’s timezone

The single most common cron surprise. Cron evaluates your expression in the machine’s local timezone, which is often UTC on cloud servers. A schedule written as 0 9 * * * (“9 a.m.”) fires at 01:00 in Shanghai when the server is on UTC. Before debugging anything else, run date on the actual machine, or in Kubernetes check the CronJob controller’s zone. When collaborating across regions, translate expectations with a timezone converter and pin the schedule to UTC explicitly where your platform allows it.

Misunderstanding #2: day-of-month and day-of-week OR together

If both day fields are restricted (neither is *), cron runs when either matches — they are OR’d, not AND’d. The expression 0 0 13 * 5 does not mean “the 13th, if it is a Friday”; it means “the 13th of every month, and every Friday”. This behavior comes straight from the POSIX spec and surprises nearly everyone once.

Quartz-based schedulers (Spring, some job platforms) solve this differently: there you write ? in the field you do not constrain — 0 0 0 13 * ? is unambiguous.

Misunderstanding #3: a missed window is skipped, not made up

If the machine is down, suspended, or the previous run is still executing at the scheduled time, classic cron simply skips the run — it does not fire retroactively. For jobs where a missed run matters (nightly backups, invoice generation), add a “did it actually run?” reconciliation, or use a scheduler with miss policies (systemd timers with Persistent=true, Kubernetes startingDeadlineSeconds, Quartz misfire instructions).

Debugging workflow that catches 90% of issues

  1. Decode the expression into “next N runs” and confirm they are what you intended — our Cron Tool shows the next executions and accepts both 5-field and 6-field (with seconds) forms.
  2. Confirm the timezone of the runner, and re-read the schedule in that zone.
  3. Check the step arithmetic: */7 in minutes fires at :00, :07, :14… and wraps unevenly across the hour — fine if intended, surprising if not.
  4. Watch one real trigger in staging before shipping the schedule to production.

Cron is a five-decade-old syntax that still outperforms its reputation — most “cron bugs” are timezone and semantics misunderstandings, and all of them are checkable in under a minute with the right tool.