DevTools Hub

Search tools

Search for a developer tool

Understanding Cron Expressions

A cron expression packs an entire recurring schedule into five terse fields — which is exactly why it's so easy to misread one at a glance. 0 9 * * 1-5 and 9 0 * * 1-5 look almost identical and mean completely different things. The syntax itself is small, though: once a handful of rules click, you can read (and write) almost any schedule you'll actually encounter.

The five fields

A standard cron expression is five space-separated fields, always in this order:

minute   hour   day-of-month   month   day-of-week
0-59     0-23   1-31           1-12    0-6 (0 = Sunday)

There's no field for seconds and no field for year in standard cron — five fields, every time. (Some schedulers, like Quartz, add a seconds field or a year field; that's a different format, not a variant of the same one.)

The four symbols that do all the work

  • * — "any value." * * * * * means every minute of every hour of every day.
  • a number — an exact value. 0 9 * * * means minute 0, hour 9 — i.e. 9:00am, every day.
  • a range (-) — inclusive on both ends. 1-5 in the day-of-week field means Monday through Friday.
  • a list (,)1,15 in the day-of-month field means the 1st and the 15th, and nothing in between.
  • a step (/)*/15 in the minute field means every 15 minutes, starting from the field's minimum (so :00, :15, :30, :45). Steps combine with ranges too: 9-17/2 means every 2 hours between 9am and 5pm.

Building up from simple to specific

*/15 * * * *      Every 15 minutes
0 * * * *         Every hour, on the hour
0 9 * * *         Every day at 9:00am
0 9 * * 1-5       Every weekday at 9:00am
0 9 1 * *         The 1st of every month at 9:00am
*/15 9-17 * * 1-5 Every 15 minutes, 9am-5pm, on weekdays

Reading left to right — minute, then hour, then day-of-month, then month, then day-of-week — each field just narrows down which minutes qualify. An empty-feeling field (a bare *) contributes no restriction at all.

The trap: day-of-month and day-of-week together mean OR, not AND

This is the single most common source of cron confusion. Everywhere else, adding a restriction narrows the schedule down. But if both day-of-month and day-of-week are restricted (neither is a bare *), standard cron matches a date that satisfies either one — not both:

0 0 1 * 1    Runs at midnight on the 1st of the month
             AND ALSO at midnight every Monday
             — not "the 1st, only if it's a Monday"

If you actually want "only if both conditions hold" (e.g. "the 1st, but only when it falls on a Monday"), plain cron syntax can't express that — you need the job itself to check the date and exit early, or a scheduler with richer expressions. The fix for the OR trap is usually simpler: leave one of the two fields as *. If you only meant to restrict by day-of-week, don't also restrict day-of-month (and vice versa) — the moment both are restricted, you've opted into OR semantics whether you meant to or not.

Nicknames: @daily, @hourly, and friends

Many cron implementations (including Vixie cron and most Linux distributions) accept a handful of shorthand strings in place of the five fields:

@yearly   (or @annually)   0 0 1 1 *
@monthly                    0 0 1 * *
@weekly                     0 0 * * 0
@daily    (or @midnight)    0 0 * * *
@hourly                     0 0 * * * (fires at minute 0 of every hour)
@reboot                     runs once, at startup — no fixed-field equivalent

They're convenient, but not universal — not every scheduler (or every cron-parsing library, including some used to build tooling like this) supports them. If you're not sure your target environment does, the numeric five-field form on the left is the portable choice.

Mistakes worth double-checking

  • 24-hour clock. 0 9 * * * is 9am; 0 21 * * * is 9pm. There's no am/pm field, and a typo here is silent — the job just runs 12 hours off from what you intended.
  • Sunday can be 0 or 7. Most implementations accept both as Sunday for compatibility, but don't rely on 7 if you need to double-check against a stricter parser — 0 is the safer, more universally understood choice.
  • It runs in the system's timezone, not necessarily yours. A cron job on a server is evaluated against that server's configured timezone (often UTC), which may not match your local time. This is the single most common reason a schedule seems to fire "a few hours off" from what was intended.
  • Step values don't always divide evenly. */7 in the hour field doesn't mean "every 7 hours forever" — it means every hour that's a multiple of 7 within the field's range (0, 7, 14, 21), so the gap between the last match and midnight is shorter than 7 hours.

Try it yourself

Don't hand-write an expression from scratch if you don't have to — Cron Builder generates one from a plain schedule (interval, daily, weekly, monthly, or custom fields) with a live preview of the next run times. To go the other way — turn an expression you found into plain English — use Cron Parser. And before you commit a crontab file to a server, run it through Cron Validator to catch malformed fields, reversed ranges, and missing commands before they cause a job to silently never run. All three run entirely in your browser.

Related tools