Cron expressions explained: fields, steps and time zones

Updated:

Cron is the scheduler that runs commands at set times on Unix-like systems, and its five-field schedule format has spread far beyond it: GitHub Actions, Kubernetes and many hosting panels use the same notation. A line such as 30 2 * * 1 reads “at 02:30 every Monday”. Most of the format is easy; a few rules are not, and those are the ones behind jobs that run at the wrong time, twice, or never. This guide follows the POSIX standard and cronie, a widely used Linux cron, and notes where other schedulers differ.

The five fields

A crontab line has five time fields separated by spaces, followed by the command. The job runs in every minute that matches all of them, with one exception for the two day fields, explained below. The standard numbers the days of the week from 0 to 6, with 0 for Sunday; cronie and most Linux crons also accept 7 for Sunday, and three-letter English names such as mon or jan, in any case.

FieldValuesExampleMeans
Minute0–5930at minute 30
Hour0–232at 02:00–02:59
Day of month1–311,15on the 1st and the 15th
Month1–12 or jan–dec*every month
Day of week0–7 or sun–sat (0 and 7 are Sunday)1-5Monday to Friday

An asterisk means every value of the field. Times use the 24-hour clock, so 9 is 09:00 and 21 is 21:00.

Lists, ranges and steps

A field can hold a list (1,15), a range (9-17) or a step. A step divides a range: 0-30/10 in the minute field means minutes 0, 10, 20 and 30, and */15 means every value from the first, so minutes 0, 15, 30 and 45. Steps are counted from the start of the range and start again at every new hour, day or month; they do not run on from the previous one.

Only intervals that divide 60 evenly (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30) give equal gaps in the minute field. Every 90 minutes needs two lines: 0 0-21/3 * * * runs at 00:00, 03:00 … 21:00, and 30 1-22/3 * * * at 01:30, 04:30 … 22:30.

cronie also accepts shortcuts: @hourly, @daily (0 0 * * *), @weekly (0 0 * * 0), @monthly (0 0 1 * *), @yearly or @annually (0 0 1 1 *) and @reboot, which runs once at startup. They are not part of the standard: GitHub Actions, for example, does not accept them.

Cron expression generator

Day of month or day of week

When both the day-of-month and the day-of-week fields hold something other than an asterisk, the standard says a day matches if either one matches, not both. 0 9 13 * 5 therefore runs at 09:00 on every 13th and on every Friday, not only on Friday the 13th. In November 2026 that is the 6th, the 13th (a Friday, which runs once), the 20th and the 27th.

To combine the two, schedule one field and let the command check the other. In a crontab, a % in the command is turned into a new line, so it has to be written \%:

Time zones and daylight saving

Cron reads the schedule in the time zone of the machine or service that runs it, which is often UTC on servers and hosted platforms. 0 9 * * * on a UTC server runs at 09:00 or 10:00 in Lisbon and London, and at 10:00 or 11:00 in Paris, Berlin, Madrid or Rome, depending on the season. Brazil has no daylight saving, so in São Paulo it is always 06:00. cronie lets a crontab set its own zone with the CRON_TZ variable; GitHub Actions takes a timezone key next to cron; Kubernetes has the .spec.timeZone field, stable since version 1.27, and refuses CRON_TZ or TZ written inside the schedule.

Daylight saving makes some local times disappear or repeat. On 29 March 2026 the clocks in Paris, Berlin, Madrid and Rome jump from 02:00 to 03:00, so 30 2 * * * names a time that does not exist; on 25 October 2026 the hour from 02:00 to 03:00 happens twice. In Lisbon and London it is the hour from 01:00 to 02:00.

SchedulerJob in the skipped hourJob in the repeated hour
cronie (changes under 3 hours, jobs at a fixed time or less often than hourly)runs immediately after the changeruns once
GitHub Actions (with timezone)moves to the next valid time: 02:30 becomes 03:00not stated
AWS EventBridge Scheduler (with a time zone)skippedruns once
Schedules kept in UTCnot affectednot affected

Jobs that run more often than once an hour are scheduled normally by cronie, and its crontab manual warns in general that missing times never match and repeated ones can run twice, so the behaviour differs between crons. The safe choices are to keep the schedule in UTC, or to avoid the hours between 01:00 and 03:00 local time for jobs that must run exactly once a day.

The environment a job runs in

A cron job does not get the environment of your terminal. The standard runs the command with sh and a minimal set of variables (HOME, LOGNAME, PATH and SHELL), so a command that works when you type it can fail in cron because a program is not in PATH or a variable is missing. Use full paths, or set PATH at the top of the crontab.

Anything the command prints is mailed to the owner of the crontab; cronie sends it to the address in MAILTO instead, and sends nothing when MAILTO is empty. On a server without mail set up, that output is easy to lose, so send it to a log file instead.

Cron starts each run on time whether or not the previous one has finished, so a job that sometimes takes longer than its interval can end up running twice at once. flock from util-linux holds a lock file while the command runs; with -n it gives up at once, with exit status 1, if the lock is taken. Kubernetes has the same choice built in: concurrencyPolicy is Allow by default, and can be set to Forbid (skip the new run) or Replace (stop the old one).

The same schedule on other platforms

The five-field format is not universal. Some schedulers add seconds or a year, some require a question mark in one of the two day fields because they do not support the either-or rule, and some number the days of the week from 1. “Every weekday at 09:00” in each:

SchedulerExpressionNotes
Linux cron (cronie)0 9 * * 1-5Machine's time zone, or CRON_TZ.
GitHub Actions0 9 * * 1-5UTC unless timezone is set; at most once every 5 minutes; no @ shortcuts.
Kubernetes CronJob0 9 * * 1-5Controller's time zone, or .spec.timeZone.
AWS EventBridgecron(0 9 ? * MON-FRI *)Six fields with a year; ? in one day field; days 1–7 start on Sunday; scheduled rules use UTC.
Quartz (Java)0 0 9 ? * MON-FRISeconds first, optional year; ? in one day field; days 1–7 start on Sunday.
systemd timerOnCalendar=Mon..Fri *-*-* 09:00:00Weekday and date must both match; *-*~01 is the last day of the month.

When you copy a schedule between systems, check the number of fields and how the days of the week are numbered: 1 is Monday in cron but Sunday in Quartz and EventBridge.

Why a job did not run

Before relying on a schedule, read it back field by field, and on Linux check the next runs: systemd-analyze calendar shows them for a timer expression.

Frequently asked questions

Is Sunday 0 or 7?

Both, in cronie and most Linux crons. The POSIX standard only defines 0 to 6, with 0 for Sunday, so 0 is the portable choice.

Can cron run a job every 30 seconds?

No: the smallest unit is the minute. Schedulers with a seconds field, such as Quartz, can; on Linux a systemd timer can too.

How do I run a job every 90 minutes?

With two lines: 0 0-21/3 * * * and 30 1-22/3 * * *. Together they run every hour and a half, from 00:00 to 22:30.

What does 0 0 * * 0 mean?

Midnight at the start of every Sunday. It is the same as @weekly in cronie.

Why does my job run on days I did not choose?

Probably because both day fields are set. Standard cron runs the job when either the day of the month or the day of the week matches.

References

  1. The Open Group Base Specifications (POSIX.1-2024) — crontab
  2. cronie — crontab(5): tables for driving cron
  3. cronie — cron(8): daylight saving time and other time changes
  4. util-linux — flock(1)
  5. GitHub Docs — Events that trigger workflows: schedule
  6. Kubernetes documentation — CronJob
  7. Amazon EventBridge — Cron expressions for scheduled rules
  8. Amazon EventBridge Scheduler — Schedule types (time zones and daylight saving)
  9. Quartz Scheduler — CronTrigger tutorial
  10. systemd — systemd.timer(5) and systemd.time(7): calendar events

Related guides