All posts

Cron syntax explained with examples

Read any cron expression: the five fields, * , - / ? L W #, ten ready-made schedules, the day-of-month OR day-of-week rule, time zones, DST and silent failures.

By Last reviewed 7 min readCronSchedulingDevOps

A cron expression packs a schedule into five space-separated fields: 30 2 * * 1-5 means 02:30, Monday to Friday. What it leaves out causes most of the trouble: the time zone, whether the scheduler expects a seconds field, and what happens when both day fields are set.

Paste any expression into the cron expression parser, which explains each field and lists the next 10 run times in a time zone you choose. It accepts 5, 6 and 7 fields, the @ shortcuts and L, W and #. The run times below come from its code with "now" fixed at Friday 30 October 2026, 12:00 UTC, and are in UTC unless stated.

The five fields

minute        0–59
hour          0–23
day of month  1–31
month         1–12 or JAN–DEC
day of week   0–6 or SUN–SAT (Vixie cron also takes 7 for Sunday)
SyntaxMeaningExample
*Every value* in hour: every hour
,List9,17 in hour: 09:00 and 17:00
-Inclusive range1-5 in day of week: Monday to Friday
/Step*/15 in minute: 0, 15, 30, 45

Names are case-insensitive. Cron's resolution is one minute; seconds need a dialect.

Seconds, years and other dialects

SystemFieldsWeekday numbers
Vixie cron, cronie, Kubernetes, GitHub Actionsmin hour dom mon dow0–6, Sunday = 0
Springsec min hour dom mon dow0–7, Sunday = 0 or 7
Quartzsec min hour dom mon dow [year]1–7, Sunday = 1
AWS EventBridgecron(min hour dom mon dow year)1–7, Sunday = 1

The last column is the dangerous one: 1-5 is Monday to Friday in cron and Spring but Sunday to Thursday in Quartz and AWS. Names mean the same wherever they're accepted, so prefer MON-FRI. The parser says which layout it detected, reads seconds-first weekday numbers the Spring way and warns that Quartz differs. Paste AWS expressions without the cron( ) wrapper.

The extensions: ? L W #

  • ? means no specific value, in the day fields only. Quartz and AWS require it in one of the two; Spring and Kubernetes treat it as *.
  • L in day of month is the last day (30 November, 28 February). After a weekday it means the last one in the month: 5L is the last Friday in Spring, 6L in Quartz and AWS.
  • W is the nearest weekday in the same month. 15 November 2026 is a Sunday, so 15W fires on Monday the 16th. LW is the last weekday.
  • # is the nth weekday: MON#1 is the first Monday (2 November, 7 December).

L, W and # come from Quartz and also work in Spring and AWS, but not in Vixie cron or cronie, and the Kubernetes and GitHub Actions docs don't list them.

The day-of-month OR day-of-week rule

In Vixie cron and cronie, when both day fields are restricted, a day matches if either field does. The crontab man page's own example, 30 4 1,15 * 5, runs on the 1st and 15th plus every Friday. So 0 0 1 * MON doesn't mean "the 1st, if it's a Monday": it runs on Sunday 1 November, every Monday, and Tuesday 1 December. For the first Monday, restrict one field and test the other in the command (\% is explained under debugging):

0 9 1-7 * * [ "$(date +\%u)" = 1 ] && /usr/local/bin/report.sh

One twist: cronie's source only checks whether a day field starts with *, so 0 0 */2 * MON falls back to AND and means Mondays on odd dates only (9 and 23 November). The parser applies the same rule.

Ten everyday schedules

ScheduleExpression
Every 5 minutes*/5 * * * *
Every hour at :000 * * * *
Every day at midnight0 0 * * *
Weekdays at 09:000 9 * * 1-5
Every Sunday at midnight0 0 * * 0
First day of the month0 0 1 * *
Last day of the monthQuartz and Spring: 0 0 0 L * ?; crontab: see below
Every 15 minutes in business hours*/15 9-17 * * 1-5
Twice a day, 09:00 and 21:000 9,21 * * *
Quarterly, 1 Jan/Apr/Jul/Oct0 0 1 */3 *

For Quartz or Spring, add a seconds field in front and, for Quartz, a ? in the day field you don't use: weekdays at 09:00 becomes 0 0 9 ? * MON-FRI. Note that 9-17 includes the whole 17:00 hour: from Friday 17:20 the parser lists 17:30, 17:45, then Monday 09:00. To stop at 17:00, use 9-16 plus a second line, 0 17 * * 1-5. And quarterly works because steps start at the field's minimum, so */3 in month is 1, 4, 7, 10.

Last day of the month without L

In a crontab, run on every candidate day and let the command check that tomorrow is the 1st:

# GNU date; on macOS and BSD use date -v+1d +\%d
55 23 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /usr/local/bin/month-end.sh

Or run at 0 0 1 * * and process the previous month.

Why */7 is not every 7 days

A step walks through one field and starts again every period. */7 in day of month is the list 1, 8, 15, 22, 29, so 0 0 */7 * * runs on 29 November and again on 1 December, two days later. Likewise, */35 in minutes fires at :00 and :35; the parser's run list shows 12:35, then 13:00. For weekly, name a day (0 0 * * 1). For a true N-day interval, count days since the epoch:

# every 14 days at 03:00 (whole UTC days since 1970)
0 3 * * * [ $(( $(date +\%s) / 86400 \% 14 )) -eq 0 ] && /usr/local/bin/job.sh

Time zones and daylight saving time

The scheduler, not the expression, sets the time zone:

  • Vixie cron and cronie: the daemon's local zone; cronie also accepts a CRON_TZ=Europe/Berlin line for the whole crontab.
  • Kubernetes: the kube-controller-manager's zone, unless you set .spec.timeZone (stable since v1.27). CRON_TZ or TZ inside the schedule fails validation.
  • GitHub Actions: UTC, unless you add an IANA timezone key next to cron.
  • AWS EventBridge: scheduled rules use UTC; EventBridge Scheduler accepts a time zone.
  • Spring: the server's zone, or the zone attribute of @Scheduled.

With the parser set to America/New_York, 0 9 * * * is 13:00 UTC until 31 October and 14:00 UTC from 1 November 2026, when DST ends; a fixed UTC schedule can't follow that shift. To check a run time in another zone or as a Unix timestamp, use the timestamp converter, which shows UTC, local time and one extra zone.

DST also removes one hour a year and repeats another. The parser follows the wall clock literally; in America/New_York:

30 1 * * *   Sun 1 Nov 2026 at 01:30 EDT (05:30Z) and again at 01:30 EST (06:30Z)
30 2 * * *   Sat 13 Mar 2027, then Mon 15 Mar: no 02:30 on the 14th

cronie and Debian's cron special-case clock changes under three hours for fixed-time jobs: a job in the skipped hour runs right after the jump, and one in the repeated hour runs only once. GitHub Actions with a timezone moves a skipped 02:30 to 03:00, and Quartz's docs warn of skipped or doubled triggers. If a job must run exactly once a day, avoid the small hours or schedule it in UTC.

Shortcuts: @reboot, @daily and friends

ShortcutSame as
@yearly, @annually0 0 1 1 *
@monthly0 0 1 * *
@weekly0 0 * * 0
@daily, @midnight0 0 * * *
@hourly0 * * * *
@rebootOnce, when the cron daemon starts

Vixie cron and cronie support all of them. Kubernetes and Spring document all but @reboot (Spring with a seconds field in front), and GitHub Actions supports none.

Debugging a job that never ran

  1. Did cron try? Look for a CMD line at the expected minute in /var/log/syslog or journalctl -u cron (Debian, Ubuntu), or /var/log/cron or journalctl -u crond (RHEL, Fedora). No line means a wrong schedule or time zone.
  2. Bare environment. cron skips your shell profile and runs jobs under /bin/sh with a short PATH. Use absolute paths or set PATH= in the crontab; a temporary * * * * * env > /tmp/cron-env.txt shows what jobs get.
  3. Percent signs. An unescaped % becomes a newline, and the rest of the line goes to standard input. Write \%.
  4. Lost output. Output is mailed to the owner or MAILTO; with no mail server, Debian and Ubuntu log No MTA installed, discarding output, so redirect it to a file.
  5. The file. Scripts need their execute bit (see chmod 755 vs 644). Lines in /etc/crontab and /etc/cron.d/ need a user name before the command, and a crontab's last line needs a newline.
# Broken: the command ends at the first %
0 2 * * * tar czf /backups/site-$(date +%F).tgz /var/www

# Fixed, output kept
0 2 * * * tar czf /backups/site-$(date +\%F).tgz /var/www >> /var/log/backup.log 2>&1

Next steps

Paste the expression from your crontab, CronJob or workflow into the cron parser, set your scheduler's real time zone, and compare the run list with what you expect. If cron fires on time but your logs or API disagree, read debugging timezone errors in APIs.

Paste anything. devprix picks the tool.

Press ⌘K on any page, paste JSON, a JWT, a timestamp or a URL, and the right tool opens with your data filled in.