Skip to main content
Productivity

Cron expressions: read one in a minute, write one without guessing

Cron has been around since the 1970s and the syntax still trips people up. Part of the problem is that a wrong expression almost never fails loudly. It just runs at 3 AM instead of 3 PM, or runs six times instead of one, and nobody notices for weeks.

Reading the five fields

A standard expression is minute, hour, day of month, month, day of week:

*/15 9-17 * * 1-5

Read it out loud and it says: every 15 minutes, between 09:00 and 17:00, any day of the month, any month, Monday through Friday. That one sentence is most of what you need. Everything else is shorthand:

  • * means every value in that field.
  • */15 means every 15th value starting from the field minimum.
  • 9-17 is a range, 1,7,15 is a list.
  • Day of week runs 0-6 with 0 as Sunday in classic cron; some schedulers accept 7 for Sunday too.

The classic mistakes

These cause most of the silent misfires:

  • Day-of-month AND day-of-week. Standard cron ORs them when both are restricted. 0 9 1 * 1 runs on the 1st AND every Monday, not "first Monday". This single rule explains half of all surprised stares.
  • Timezones. The server clock, not your clock. A cron set for 09:00 on a UTC server fires at noon in Riga. Modern schedulers let you set CRON_TZ or a timezone per job; classic crontab does not.
  • */10 on minutes combined with an hour range. */10 9 * * * fires at 9:00, 9:10... 9:50, and then again at every hour boundary after. If you wanted "every 10 minutes during business hours only", that expression already leaks outside the range.
  • Monthly jobs on the 31st. 0 9 31 * * runs in seven months of the year and silently skips the rest. If you meant "end of month", cron cannot say that directly.

Checking before you commit

The cheapest check is a dry run: paste the expression into the unix timestamp converter, which converts between Unix timestamps and plain dates in both directions. If you wrote 0 9 1 * 1 expecting the first Monday and the preview says "Oct 1" and "Oct 5", you just saved yourself a confusing month.

The rest of the job config matters just as much. Quartz adds a seconds field and a different day-of-week numbering; AWS EventBridge and GitHub Actions schedulers each have their quirks, so check which dialect your scheduler actually speaks before copying an expression between them.

Two habits worth keeping

First, write the timezone next to the schedule in whatever runs the job. 0 9 * * * in a config means nothing to the person debugging it in six months. Second, log the actual fire time inside the job. When a run happens at the wrong hour, the log entry tells you in one glance whether the schedule or the clock was to blame.

For the rest of the scheduling stack, the Unix time converter and the date difference tool cover the other time math that shows up next to cron in most codebases.

Summary

Cron is a language with one dialect per platform and no compiler warnings. Read the five fields out loud, remember the day-of-month OR day-of-week rule, and preview the next fire times before committing. Thirty seconds of checking beats a month of jobs running at the wrong hour.