Cron expressions demystified

6 min read
cron
devops

A cron expression is five fields, each describing when a job should run, read left to right from the smallest unit of time to the largest.

The five fields

* * * * *
│ │ │ │ │
│ │ │ │ └── day of week (0-6, Sunday = 0)
│ │ │ └──── month (1-12)
│ │ └────── day of month (1-31)
│ └──────── hour (0-23)
└────────── minute (0-59)

An asterisk means "every value in this field". `* * * * *` runs every minute of every hour of every day.

Common patterns worth memorizing

  • `0 * * * *` — once an hour, on the hour.
  • `0 0 * * *` — once a day, at midnight.
  • `0 9 * * 1-5` — 9am, Monday through Friday.
  • `*/15 * * * *` — every 15 minutes.
  • `0 0 1 * *` — midnight on the first of every month.
  • `0 0 * * 0` — midnight every Sunday.

The `*/n` syntax means "every n units starting from the field's minimum" — `*/15` in the minutes field fires at :00, :15, :30, :45.

Ranges and lists

Fields accept ranges (`1-5`) and comma-separated lists (`1,15`), and can combine them: `0 9,13,17 * * 1-5` runs at 9am, 1pm and 5pm on weekdays.

Day-of-month and day-of-week interact with OR, not AND

This one trips people up constantly: if both the day-of-month and day-of-week fields are restricted (neither is `*`), the job runs when *either* condition is true, not both. `0 0 1 * 1` runs on the first of the month *and* every Monday — not only on Mondays that happen to be the first.

The timezone trap

Cron itself has no concept of timezone — it runs according to the system clock of whatever host or scheduler executes it. A schedule written as `0 9 * * *` assuming "9am for our users in New York" will silently run at 9am UTC if the scheduler's system timezone is UTC, which is the default on most cloud infrastructure and CI schedulers (GitHub Actions cron, for instance, is always UTC).

The fix is to always compute the schedule in the execution environment's timezone explicitly, or convert the intended local time to UTC before writing the expression, and to re-derive it if daylight saving time changes the offset.

Seconds and non-standard extensions

Some schedulers (Quartz, some CI systems) use a six-field format with seconds as the first field, and some add `L` (last day of month/week) or `W` (nearest weekday) as extensions. These are not part of standard POSIX cron and will fail silently or error out if pasted into a system expecting five fields — check which dialect a given tool expects before assuming portability.

Reading an unfamiliar expression

Given `30 2 1,15 * *`, work left to right: minute 30, hour 2 (so 2:30am), on day 1 and day 15 of the month, any month, any day of week. That reads as "2:30am on the 1st and 15th of every month" — a typical semi-monthly batch job schedule.

Building instead of memorizing

For anything beyond the handful of common patterns above, build the expression field by field with an interactive builder and verify it against a parser that shows the next several run times in plain language — a single off-by-one in a range (`1-5` vs `0-4` for weekdays) is easy to miss by eye and very visible once you see the actual computed run times.

Tools from this article

← All articles