design

Cron Syntax Cheatsheet (2026): Read and Build Any Crontab Expression

Cron syntax explained: the 5-field order, a 10-second reading model, 16 verified recipes, the day-of-week OR gotcha, DST traps, and cron vs systemd timers.

Published 2026-07-04 · 10 min read

Affiliate disclosure

Some links below are affiliate links. I may earn a commission from qualifying purchases at no extra cost to you. I only recommend tools I have used or tested.


“Talk is cheap. Show me the code.”
― Linus Torvalds
Photo by Mohammad Rahmani on Unsplash

TL;DR

  • Five fields, in this order: minute hour day-of-month month day-of-week. Leave any field you don't care about as *.
  • The gotcha that bites everyone: if you restrict both day-of-month and day-of-week, Vixie cron runs the job when either matches (an OR, not an AND). Restrict only one and leave the other *.
  • Test yours before you deploy: paste any expression into the Cron Parser to see the exact next run times in plain English.

You need a script to run every night at 2 AM. You open crontab -e, stare at five asterisks, and realise you're not sure which one is the hour. This guide fixes that permanently: read any cron line in ten seconds, build the one you need from a verified recipe, and know the two or three traps that silently break jobs in production.

How do you read any cron expression in 10 seconds?

Read it left to right as five fields, always in this exact order:

┌───────────── minute        (0-59)
│ ┌─────────── hour          (0-23)
│ │ ┌───────── day of month  (1-31)
│ │ │ ┌─────── month         (1-12)
│ │ │ │ ┌───── day of week   (0-7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *  command to run

The whole mental model: you only ever control three things — the minute, the hour, and one day-selector — and leave every field you don't care about as *. An asterisk means "every value of this field," so * * * * * runs the command every single minute, forever. Every recipe you'll write is that baseline with one to three fields pinned down.

So 30 2 * * * reads as: minute 30, hour 2, every day-of-month, every month, every day-of-week — i.e. 2:30 AM every day. The command after the fifth field is what runs; it isn't a sixth time field. That's the whole trick. Field ranges and the asterisk meaning are defined in the crontab(5) man page and the POSIX crontab spec; when in doubt, verify a line in the Cron Parser instead of guessing.

What does each of the five fields mean?

Each field has a fixed range and a couple of quirks worth knowing up front. This is the reference you'll come back to.

FieldRangeNotes
Minute0-59Minute of the hour. 0 is the top of the hour.
Hour0-2324-hour clock, system local time. 0 is midnight, 23 is 11 PM.
Day of month1-31Starts at 1, not 0. There is no native "last day of month."
Month1-121 is January. Three-letter names (JANDEC) also work in most implementations.
Day of week0-70 and 7 both mean Sunday, 1 is Monday, 6 is Saturday. Names (SUNSAT) also work.

Two range facts trip people up. Day-of-month starts at 1 while the time fields (minute, hour) start at 0. And day-of-week has two Sundays: the crontab(5) man page defines it as 0-7 where "0 or 7 is Sunday," so both 0 (US week-start) and 7 (ISO week-end) resolve correctly. The POSIX spec is stricter — 0-6 with 0=Sunday — so on a minimal POSIX cron, stick to 0-6 and don't rely on 7.

What do *, ,, -, and / do?

Four characters cover every standard cron pattern. Anything beyond these is a non-standard extension you should not assume is supported.

CharacterNameMeaningExample
*AsteriskEvery value ("first–last") of the field* * * * * = every minute
,CommaA list of specific values0 0,12 * * * = midnight and noon
-HyphenAn inclusive range0 9-17 * * * = every hour 9 AM–5 PM
/SlashA step ("every Nth") over a range or **/5 * * * * = every 5 minutes

The one subtlety is what the step counts from. A step on an asterisk starts at the field's first value: */5 in the minute field means minutes 0, 5, 10 … 55. A step on an explicit range starts at the range's low end: 10-50/5 means 10, 15, 20 … 50 — it starts at 10, not 0. Get this wrong and you get "why did it fire at a weird minute" bugs, so test a stepped schedule in the Cron Parser if it surprises you.

Everything else — L (last), W (nearest weekday), # (nth weekday of month), ? (no-specific-value), and a leading seconds field — is non-standard. Per Wikipedia's cron reference, those characters "exist only in some cron implementations, such as the Quartz Java scheduler," and the seconds field appears only in 6- and 7-field variants. Classic Vixie/POSIX cron (the crontab -e on your box) has none of them. Copy a 0 0 12 * * ? expression from a Quartz tutorial and it won't parse in a normal crontab — that leading 0 is a seconds field classic cron doesn't have.

The recipe table — 16 verified expressions to copy

Every expression below has been checked field-by-field against the ranges above. Copy the one you need; when you tweak it, verify the result in the Cron Parser.

ExpressionPlain English
*/5 * * * *Every 5 minutes
*/15 * * * *Every 15 minutes
*/30 * * * *Every 30 minutes (on the hour and half-hour)
0 * * * *Every hour, on the hour
0 */2 * * *Every 2 hours (00:00, 02:00, 04:00 …)
0 */6 * * *Every 6 hours (00:00, 06:00, 12:00, 18:00)
0 0 * * *Every day at midnight
0 9 * * *Every day at 9:00 AM
0 0,12 * * *Twice a day — midnight and noon
0 9 * * 1-59:00 AM on weekdays (Mon–Fri)
0 9-17 * * 1-5Every hour from 9 AM to 5 PM, weekdays
0 0 * * 0Every Sunday at midnight
0 8 * * 6Every Saturday at 8:00 AM
0 18 * * 1,3,56:00 PM on Mon, Wed, and Fri
0 0 1 * *Midnight on the 1st of every month
0 0 1 1,4,7,10 *Midnight on the 1st of Jan, Apr, Jul, Oct (quarterly)

Notice the pattern: every single one is * * * * * with only the fields it cares about pinned down. "Every day at 9 AM" (0 9 * * *) touches just the minute and hour and leaves the three day/month fields as *. "Weekdays 9 to 5" (0 9-17 * * 1-5) touches the minute, an hour range, and the day-of-week — and critically, it leaves day-of-month as * so the OR gotcha (next section) never triggers.

Why does restricting both day fields cause an OR, not an AND?

Because that's how Vixie cron is specified — and it's the single most surprising rule in the whole system.

Normally, cron requires all fields to match before it runs a job (a logical AND). The day fields are the exception. Per the crontab(5) man page, verbatim:

If both fields are restricted (i.e., are not *), the command will be run when either field matches the current time.

The POSIX spec says the same ("any day matching either the ... day of month, or the day of week, shall be matched"), as does the original Vixie cron crontab(5). So take this expression:

0 0 1,15 * 5    →  midnight on the 1st, the 15th, AND every Friday

If you meant "midnight on the 1st and 15th, but only if it's a Friday," this is wrong — it fires on the 1st, the 15th, and every Friday, because the two day fields are OR'd. The fix is baked into the reading model: restrict only one day-selector and leave the other as *. Want a specific day-of-month? Set day-of-week to *. Want a specific weekday? Set day-of-month to *. Restrict both only when you deliberately want the union. Cron has no built-in AND for the day fields; if you truly need "the 15th only if it's a Friday," gate it inside the script. Any expression restricting both day fields is worth pasting into the Cron Parser to see the real run dates first.

What are the @ shortcuts (@daily, @hourly, @reboot)?

Cron ships eight named shortcuts that expand to a standard expression, so you rarely need to hand-write the common ones. These are documented in crontab(5):

ShortcutEquivalentMeaning
@yearly / @annually0 0 1 1 *Once a year, midnight on Jan 1
@monthly0 0 1 * *Once a month, midnight on the 1st
@weekly0 0 * * 0Once a week, midnight on Sunday
@daily / @midnight0 0 * * *Once a day, at midnight
@hourly0 * * * *Once an hour, on the hour
@rebootOnce, after cron starts at boot

@midnight is an exact alias for @daily (both are 0 0 * * *), and @annually is an exact alias for @yearly. @reboot is the odd one — it has no time-field equivalent because it fires once when cron starts up, not on a schedule. It's handy for starting a long-running helper at boot, but remember it runs when cron starts, which on most systems is early in boot before other services may be ready.

The gotchas that bite in production

Most cron pain isn't syntax — it's these five environmental traps. Learn them once and you'll debug the "it ran at the wrong time" tickets in seconds.

Timezone and DST. Cron uses the system's local time, not UTC, unless told otherwise — which makes daylight-saving transitions dangerous. In spring-forward, the clock jumps from 2 AM to 3 AM, so a job at 2:30 AM lands in an hour that doesn't exist and can be skipped. In fall-back, the 1–2 AM hour repeats, so a job in that window can fire twice. Modern Vixie/cronie cron has special handling for shifts under three hours that mitigates this for once-a-day jobs, but behavior varies by implementation, so the robust fix is to schedule critical jobs outside the 1–3 AM window or run the box in UTC. You can pin a zone with a CRON_TZ=America/New_York line above the job — but CRON_TZ only changes when cron fires; your script still sees the system timezone unless you export TZ yourself.

No native "last day of month." Classic cron has no L, so 0 0 31 * * simply won't run in February, April, June, September, or November. Run daily and check the date in the script, or run on the 1st and target "yesterday."

Step-on-range vs step-on-star. As above, */5 starts at 0 but 10-50/5 starts at 10. For "every 5 minutes but only in the back half of the hour," write the range explicitly.

Overlapping long runs. Cron doesn't check whether the previous invocation finished. If */5 fires a job that sometimes takes 8 minutes, two copies run at once — a frequent cause of "why did it run twice," duplicated work, or corrupted output. Wrap the command in flock (e.g. flock -n /tmp/job.lock -c 'your-command') so a second invocation exits immediately while the first holds the lock.

Minimal PATH and environment. Cron runs with a stripped-down environment and a minimal PATH (typically /usr/bin:/bin), not your shell's. A script that works in your terminal can fail under cron because a binary isn't on cron's PATH. Use absolute paths for binaries, or set PATH= at the top of the crontab.

Cron vs systemd timers vs cloud schedulers — when does each fit?

All three run things on a schedule; they differ in what they survive and what they cost to operate. Pick by failure mode, not by habit.

Cron is the universal default: on virtually every Unix box, no setup beyond the daemon already running, one line does the job. Its weaknesses are the gotchas above — it silently drops runs missed while the machine was off, has no built-in success/failure logging, and won't prevent overlaps. Reach for cron for simple, per-host, best-effort jobs.

systemd timers are the heavier, more robust option on modern Linux. A timer's OnCalendar= uses the same calendar concepts as cron, but the killer feature is Persistent=: per the systemd.timer(5) man page, when set, "the service unit is triggered immediately if it would have been triggered at least once during the time when the timer was inactive" — so a missed run catches up after downtime instead of vanishing. Timers also get journal logging, monotonic scheduling (OnBootSec=, OnUnitActiveSec=), and RandomizedDelaySec= to stagger fleets. The cost: you write a .timer and a .service unit instead of one line. Choose them when you need catch-up, logging, or dependency ordering.

Cloud schedulers (managed cron, container CronJobs, serverless schedulers) win when the job must survive the death of any single machine. They run independently of any host, centralise logging and retries, and scale horizontally. The trade-off is vendor lock-in, network dependency, and per-invocation cost. Choose them when the schedule is business-critical and can't depend on one box being up.

Whichever you pick, the expression is the same skill. The same test-before-you-ship discipline applies to regex; see the regex tester guide for that side of the toolbox.

The bottom line

Cron is five fields in a fixed order, and 95% of what you'll ever write is * * * * * with the minute, hour, and one day-selector pinned down. Learn the field order, memorise the four operators (* , - /), copy from the recipe table, and internalise the one rule that saves you from mystery runs.

Bottom line: Never restrict both day-of-month and day-of-week unless you actually want the OR — and paste every expression into the Cron Parser to read its real next run times before it touches a server.

Keep reading

regex tester and pattern builder developer tool — original hero illustration

design

Regex Tester & Pattern Builder: A Practical Guide to Patterns That Actually Work

How to build regular expressions that actually work: anchors, quantifiers, the flavor traps across engines, and the ReDoS pattern that can hang your server. Test as you go.

9 min read

Laptop with code and plant in coffee shop

design

JSON vs YAML vs TOML: When to Pick Each (a Developer's Decision Guide)

JSON vs YAML vs TOML, decided: JSON for APIs and data, YAML for Kubernetes/CI config (mind the Norway problem), TOML for explicit app config like Cargo.toml. A clear decision guide.

9 min read

It all started with just HTML, CSS, and some JavaScript.

design

Base64 Encoding Explained: When and Why Developers Use It (and the Traps)

What Base64 does (3 bytes to 4 ASCII chars, +33% size), the real use cases (data URIs, JWT, Basic auth), and the traps: it is not encryption and not compression.

8 min read

A scannable QR-style code on a green background, illustrating custom-branded QR code design

design

Custom-Branded QR Codes: Add a Logo and Colors With a Free QR Code Generator

How error correction lets you overlay a logo on a QR code, the contrast and quiet-zone rules that keep it scannable, and the static-vs-dynamic catch.

8 min read

Swatchos color swatch cards with CMYK and RGB hex values showing being used by a graphic designer on a graphic designer's desk

design

HEX to RGB to CMYK in 2026: Why the Print Color Never Matches the Screen

HEX to RGB is exact math. RGB to CMYK is a lossy guess that depends on the press, the paper, and an ICC profile. Here's the difference, the worked math, and where an online converter actually helps.

8 min read

A close up of the Google Material design sticker sheet on a MacBook.

design

Color Palette Tools for Accessibility: Why Most Palettes Fail Before You Ship

83.6% of homepages fail contrast. How to pick tools that verify accessible palettes, avoid common mistakes, and test the right combinations.

9 min read