8 min read1,625 words

Cron Expressions Explained, With Examples

A clock face with five asterisk schedule markers

Cron expressions look cryptic until you understand that each field is independent and additive rather than sequential. This guide walks through the standard five-field format, the extended six-field form some schedulers use, and the quirks around day-of-week versus day-of-month that trip people up most often. A reference like the cheat sheets at /cheat-sheets is worth keeping open while you write a schedule, since the field order is easy to get backwards.

cronschedulingreference
Share on XHacker News

The five field form

A standard cron expression has five space-separated fields, in this order: minute, hour, day of month, month, and day of week. Each field accepts a specific value, a wildcard, or a set of values, and the job runs whenever the current time matches all five fields at once.

For example, 30 4 * * * means "at 4:30, every day of the month, every month, every day of the week", which in plain terms is once a day at 04:30. Reading a cron expression left to right in this order, minute first, is the single most useful habit for avoiding mistakes.

  • Field order: minute, hour, day of month, month, day of week
  • * means every value is allowed in that field
  • 30 4 * * * runs daily at 04:30

The six field form

Some schedulers, including many Java and Quartz-based systems, add a seconds field at the front, giving six fields: second, minute, hour, day of month, month, day of week. This lets you schedule something more often than once a minute, for example every fifteen seconds, which the standard five-field format cannot express.

When reading an unfamiliar cron string, check the documentation for the specific tool before assuming the field count, since a six-field expression read as a five-field one will be shifted by one position and run at completely the wrong time.

Ranges, steps and lists

A range uses a hyphen, so 1-5 in the day-of-week field means Monday through Friday (numbering conventions vary slightly by implementation, with Sunday as either 0 or 7). A step uses a slash after a range or wildcard, so */15 in the minute field means every 15 minutes, and 0-30/10 means every 10 minutes between the top of the hour and minute 30.

A list uses commas to combine specific values, so 0,30 in the minute field runs at the top and half of every hour. These building blocks combine, so 0 9-17/2 * * 1-5 runs on the hour, every two hours between 9am and 5pm, on weekdays.

  • 1-5 is a range
  • */15 or 0-30/10 are steps
  • 0,30 is a list of specific values
  • These combine within a single field

Day of week versus day of month

When both the day-of-month and day-of-week fields are restricted (not left as a wildcard), most cron implementations, including standard Linux crontab, treat them as an OR rather than an AND. So 0 0 1 * 1 means midnight on the first of the month OR every Monday, not only on Mondays that happen to fall on the first.

This is one of the most common sources of confusion in cron schedules, and it is worth testing explicitly whenever an expression restricts both fields at once, since the intuitive reading (both conditions must hold) is usually wrong.

Time zones and daylight saving time

Cron expressions on their own have no time zone; the schedule runs according to whatever time zone the scheduling system is configured with, which is often but not always UTC. Cloud schedulers and CI systems like GitHub Actions run cron in UTC by default, so a schedule written with a local time in mind needs converting first.

Daylight saving transitions can cause a schedule to run twice or not at all on the transition day, depending on whether the clock moves forward or back through the scheduled time. If a job absolutely must run exactly once regardless of DST, scheduling it in UTC avoids the ambiguity entirely.

Common schedules

A handful of patterns cover most real scheduling needs. Once a day: 0 0 * * *. Once a week on Sunday: 0 0 * * 0. Once a month on the first: 0 0 1 * *. Every weekday morning: 0 8 * * 1-5. Every five minutes: */5 * * * *.

  • Daily at midnight: 0 0 * * *
  • Weekly on Sunday: 0 0 * * 0
  • Monthly on the first: 0 0 1 * *
  • Weekdays at 08:00: 0 8 * * 1-5
  • Every five minutes: */5 * * * *

Platform differences

Linux crontab uses the standard five-field format and runs in the system time zone unless a CRON_TZ or TZ variable is set for the crontab. GitHub Actions accepts the same five-field syntax in a workflow schedule trigger, but the shortest supported interval is five minutes and the platform documents that scheduled runs can be delayed during periods of high load, so treat the schedule as a target rather than a guarantee.

pg_cron, the PostgreSQL extension, also uses the five-field format but adds its own syntax for running a job at a fixed interval measured in seconds, which standard cron cannot express. Because these small differences exist, it is worth checking the exact grammar for the platform you are targeting rather than assuming every cron implementation behaves identically.

Building and checking an expression

The safest way to write a cron expression is to build it one field at a time, minute first, and read it back in that order before saving it. For anything non-trivial, run it through a cron parser that shows you the next several run times in plain language, since a single misplaced digit can silently change a schedule from daily to hourly.

The dev tools hub at /dev-tools includes converters and formatters that are useful for the surrounding work, such as converting between time zones or formatting timestamps, even where a dedicated cron parser is a separate, more specialised tool.

Worked example: building a schedule step by step

Say the requirement is "run a report at 6:15am on the first and fifteenth of every month, but only if that day is a weekday." Start with the minute and hour: 15 6. Day of month is a list: 1,15. Month is every month: *. Day of week needs to express Monday through Friday: 1-5. Put together, that gives 15 6 1,15 * 1-5, but remember that most cron implementations OR the day-of-month and day-of-week fields rather than AND them, so this expression actually fires on the 1st, the 15th, and every weekday, which is not the same requirement.

To get a genuine AND between day of month and weekday, cron alone cannot express it; the usual fix is to schedule the job every weekday at 6:15 and have the job itself check whether today is the 1st or 15th before doing any work, moving the day-of-month condition out of the schedule and into application logic.

Edge cases worth testing

A schedule that specifies day 31 in the day-of-month field simply does not run in months with fewer than 31 days, since cron has no concept of "last day of the month" built into the standard fields. If you need a job to run on the last day regardless of month length, most schedulers require either a separate library extension or a small script that checks the date and exits early on the wrong days.

Leap years affect a 29 February schedule the same way: it fires only in years where that date exists, and silently does nothing in the other three years, which can look like a broken schedule if nobody remembers why.

  • Day 31 silently skips months shorter than 31 days
  • 29 February only fires in leap years
  • A restricted day-of-month and day-of-week together usually means OR, not AND, on most systems
  • A typo swapping the minute and hour fields runs the job at a completely different time of day

Debugging a schedule that never fires

Start by confirming the scheduler is actually reading the file or configuration you edited; a crontab edited with the wrong user, or a workflow file with a YAML indentation error, will silently fail to register the new schedule at all. Next, check the time zone the scheduler actually runs in, since a schedule that looks correct in your local time zone can be many hours off if the system runs in UTC.

If the job still does not run, verify the expression itself with a parser that lists the next several execution times in plain language rather than trying to mentally simulate five interacting fields, since a single stray character, like a comma where a hyphen was intended, produces a schedule that is syntactically valid but semantically wrong.

  • Confirm the scheduler picked up the edited file or config
  • Check which time zone the scheduler actually runs in, not the one you assumed
  • Run the expression through a parser and read back several upcoming execution times
  • Check logs for permission or environment errors separate from the schedule itself

FAQ

Can cron run a job more than once a minute? Not in the standard five-field format. A six-field format with seconds, supported by some schedulers, or an external loop inside the job itself, is needed for sub-minute intervals.

What happens if a scheduled job is still running when the next occurrence comes around? Standard cron does not check; it starts a new instance regardless, which can cause overlapping runs on a slow job. Most production schedulers add a locking mechanism or an overlap policy specifically to prevent this.

Does cron handle time zones per job? Plain crontab does not by default, though the CRON_TZ variable, where supported, can override the time zone for an individual crontab. Cloud schedulers vary, so check the specific platform documentation rather than assuming.

Questions about the tools in this guide

Short answers about the hubs this article touches, each linking straight to the tool.

Dev Tools

Open hub

Cheat Sheets

Open hub