Most cron cheatsheets stop at the five fields and a dozen examples, which is exactly the part nobody gets wrong. The failures come from the differences between schedulers: seconds fields that sit at the front in Quartz and at the back in croniter, weekday numbers that shift by one, day fields that OR in a crontab and AND in Spring. This page is the reference for those differences. To check a concrete expression, paste it into our cron expression parser, which speaks all of these dialects, translates between them and accepts crontab.guru URLs.

The five fields

A classic crontab entry is five schedule fields followed by the command. Read it as a sentence: at minute M, past hour H, on day-of-month D, in month M, on day-of-week W.

PositionFieldValuesNotes
1minute0–59
2hour0–230 is midnight, there is no 24
3day of month1–31combines with field 5 via OR, not AND
4month1–12names JAN–DEC allowed in Vixie-family crons
5day of week0–70 and 7 are both Sunday; names SUN–SAT allowed

Two details from the crontab(5) man page that trip people up. Names are only valid as single values: MON-FRI works on most modern crons, but ranges and lists of names are outside the spec, so portable files use numbers. And a value like * in day-of-month next to a restricted day-of-week does not mean "and": with both day fields restricted, the entry fires when either matches. That rule alone produces most of the mystery schedules; the full story, along with the DST window and the % that truncates commands, is in our cron pitfalls guide. This page stays on syntax.

Special characters, and who understands them

CharacterMeaningUnderstood by
*every valueeveryone
,list: 0,15,30,45everyone
-inclusive range: 9-17everyone
/step: */5, 10-40/10Vixie extension; everywhere in practice, but not POSIX
?"no value" for the unused day fieldQuartz, EventBridge (required in one day field); alias for * in Spring and Kubernetes
Llast day of month; L-3, LW; 5L = last FridayQuartz, Spring 5.3+, Cloudflare, EventBridge
Wnearest weekday to a date: 15WQuartz, Spring 5.3+, Cloudflare, EventBridge
#nth weekday of the month: MON#1Quartz, Spring 5.3+, Cloudflare, EventBridge
Hstable per-job hash, for load spreadingJenkins; croniter with hash_id
@daily & co.macros for common schedulesVixie-family, Kubernetes, Spring; not POSIX, not GitHub Actions

The macros are @yearly, @monthly, @weekly, @daily, @midnight, @hourly and, only in real crontabs, @reboot, which fires when the daemon starts after boot. Worth knowing before relying on it: at that point the network is often not up yet, and there is no retry.

The expressions you actually came for

All lines are classic 5-field crontab. For Quartz or Spring, put a 0 in front for seconds and fix the weekday numbers (see below).

GoalExpressionNote
Every minute* * * * *the smallest interval plain cron has
Every 5 minutes*/5 * * * *fires at :00, :05, :10 …
Hourly, at half past30 * * * *
Every 2 hours0 */2 * * *at 00:00, 02:00, 04:00 …
Daily at 03:000 3 * * *in the machine timezone, usually UTC
Weekdays at 09:000 9 * * 1-51 = Monday in crontab
Every 30 min, business hours*/30 9-17 * * 1-5last run 17:30, not 17:00
Twice a day0 6,18 * * *
Sunday at midnight0 0 * * 07 works too, but not everywhere
First of the month, 00:000 0 1 * *
Quarterly0 0 1 1,4,7,10 *Jan, Apr, Jul, Oct
Last day of the month0 0 28-31 * * + guardguard: [ "$(date -d tomorrow +\%d)" = "01" ]
First Monday of the month0 9 1-7 * * + guardguard: [ "$(date +\%u)" = "1" ]; not 1-7 * 1, that ORs
Every 90 minutestwo lines, see belowa single expression cannot express it

The backslashes in the guards are load-bearing: inside a crontab, an unescaped % ends the command. And before one of these lines goes anywhere near production, run it through the parser and read the next five run times it computes; a schedule you can read back beats a schedule you believe.

Why */7 is not "every 7 minutes"

A step value does not mean "every N units of time". It means "every Nth value within the field's range, starting at the range start". The field resets at its boundary, and if the range does not divide evenly, the last interval is short:

ExpressionActually fires atInterval
*/7 * * * * (minutes):00, :07, :14, :21, :28, :35, :42, :49, :56, then :007 min, except one 4-minute gap per hour
0 0 1 */5 * (months)Jan, Jun, Nov, then Jan again5, 5, then 2 months
*/90 * * * * (minutes):00 onlysilently an hourly job

Steps that divide the range evenly (*/5, */10, */15, */20, */30 in minutes) behave exactly as read, which is why the trap survives: the common cases work. For a true every-90-minutes cadence, interleave two entries:

LineFires at
0 0-21/3 * * *00:00, 03:00, 06:00 …
30 1-22/3 * * *01:30, 04:30, 07:30 …

A step can also start from an offset: 10-59/15 in minutes fires at :10, :25, :40, :55. Since a range plus a step is Vixie syntax, a strictly POSIX cron accepts none of this; every step has to be spelled out as a list there.

Sunday is 0, 7 or 1, depending on who you ask

This is the single most damaging difference between dialects, because a shifted weekday field still parses. The schedule installs, runs, and quietly does everything one day off.

SystemSundayMondayWeek range
POSIX crontab010–6
Vixie, cronie, Kubernetes, GitHub Actions0 or 710–7
Spring 5.3+, node-cron, croniter0 or 710–7
Quartz121–7
AWS EventBridge121–7

So 0 9 * * 5 is Friday in a crontab and Thursday in Quartz or EventBridge. In Quartz-family systems, always write names (MON-FRI), which mean the same thing everywhere and remove the numbering question entirely. In portable crontabs, stick to 0–6 and never write 7.

The dialect matrix

The table we wanted every time an expression moved between a crontab, a Java service and a Python script, and never found in one place:

SchedulerFieldsSecondsL W #?MacrosBoth day fields set
POSIX cron5nonononoOR
Vixie / cronie5nononoyes, incl. @rebootOR
Quartz6–7, seconds first, year lastyes, firstyesrequired in one day fieldnoparse error
Spring 5.3+6, seconds firstyes, firstyesalias for *yesAND
Jenkins5, plus Hnononoyes, hashed (@daily = H H * * *)OR
node-cron (npm)5–6, seconds firstoptional, firstnononoOR
croniter (Python)5–6, seconds lastoptional, lastnonoyesOR, switchable to AND via day_or=False

Three rows deserve a sentence each. Quartz refuses to guess what two restricted day fields mean and throws "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented", which is at least honest. Spring intersects them, so 0 0 0 13 * FRI in Spring really is Friday the 13th, while the same idea in a crontab fires about nine times a month. And croniter puts the optional seconds field at the end of the expression unless you pass second_at_beginning=True, the exact mirror of Quartz, so a six-field expression means two different schedules in a Java and a Python codebase. The parser resolves each field per dialect and shows the OR and AND readings side by side.

Hosted cron: what the platforms accept

Every cloud scheduler wraps one of the dialects above and adds its own limits. The limits are what the docs bury:

PlatformSyntaxTimezoneWhat bites
Kubernetes CronJob5 fields + macros; ? = *UTC, or .spec.timeZone (stable since 1.27)no L W #; unquoted */5 is a YAML alias error
AWS EventBridge6 fields: no seconds, year 1970–2199 last; L W #UTC, or per-rule timezoneSunday = 1; ? mandatory in one day field
GCP Cloud Scheduler5 fields, unix-cronselectable per jobno seconds, no L W #
Azure FunctionsNCRONTAB: 6 fields, seconds firstUTC, or WEBSITE_TIME_ZONE5-field input works but shifts meaning vs. crontab
GitHub Actions5 fields, no macrosUTC, optional IANA timezone5-minute floor, queue delays, 60-day auto-disable
Cloudflare Workers5 fields plus L W #UTC onlyQuartz extras but crontab weekday numbers
Vercel5 fieldsUTC onlyHobby: one run per day, fires anywhere within the hour

Two of these deserve emphasis, because they surface as "cron is broken" tickets. GitHub Actions schedules are pulled from a shared queue: GitHub documents that runs can be delayed in periods of high load and names the start of every hour as the busiest slot, so 0 * * * * is the worst possible choice there; schedule at minute 23 and the delay mostly disappears. The auto-disable is the other one: a public repository without activity for 60 days gets its scheduled workflows switched off, announced by a single email. Vercel's Hobby plan takes the opposite approach and documents that a daily job fires at some point within the scheduled hour, which is fine for a cache warmer and a surprise for anything a human is waiting on. If a CronJob manifest is involved, our YAML footgun linter catches the unquoted */5 before kubectl does.

Translating cron to systemd OnCalendar

On any systemd machine, timers are the other native scheduler, and their calendar syntax reads weekday, then date, then time. The translation is mechanical:

CronOnCalendar=
0 3 * * **-*-* 03:00:00 (or just 03:00)
*/15 * * * **:0/15
0 9 * * 1-5Mon..Fri 09:00
0 0 1 * **-*-01 00:00:00 (= monthly)
last day of month + guard*-*~01 00:00:00, no guard needed
0 0 13 * 5 + weekday guardFri *-*-13 00:00, no guard needed

The last two rows are the argument for learning the syntax at all: systemd combines a weekday with a date as AND, so Friday the 13th is one readable line, and the ~ counts days from the end of the month. Test any candidate with systemd-analyze calendar 'Fri *-*-13 00:00', which prints the next elapse times without installing anything, the same check our parser does for cron expressions. Timers also answer the two requests plain cron cannot: Persistent=true runs a schedule missed while the machine was off, and RandomizedDelaySec=300 is the systemd spelling of Jenkins' H.

That is the whole sheet. Syntax is the half of cron you can look up; the half that pages you at night, timezones, DST windows, the silent environment, lives in the pitfalls guide.

Syntax questions the table leaves open

Why does */7 * * * * not run every 7 minutes?

Because a step applies within the field range and restarts at every hour: */7 in the minute field matches minutes 0, 7, 14, 21, 28, 35, 42, 49 and 56, then jumps back to 0, so once per hour the gap is 4 minutes instead of 7. A step only means "every N minutes" when N divides 60 evenly, so 5, 10, 15, 20 and 30 behave as read and everything else drifts. For a true 7-minute cadence you need a scheduler that measures from the previous run, such as a systemd timer with OnUnitActiveSec=7min.

How do I run a cron job every 90 minutes?

Not with a single classic expression: the minute field resets every hour, so */90 in minutes matches only minute 0 and quietly becomes an hourly job. Use two lines that interleave, 0 0-21/3 * * * and 30 1-22/3 * * *, which together fire at 00:00, 01:30, 03:00 and so on. systemd timers express it directly with OnUnitActiveSec=90min, and Quartz cannot do it in one expression either, for the same reset reason.

How do I schedule a cron job for the first Monday of the month?

In Quartz: 0 0 9 ? * MON#1, where #1 means the first occurrence of that weekday in the month. Spring 5.3+ accepts the same # token with seconds first. Plain crontab has no #, and writing 0 9 1-7 * 1 fires on every day 1 to 7 plus every Monday, because two restricted day fields combine with OR, so schedule the date window and test the weekday in the command: 0 9 1-7 * * [ "$(date +\%u)" = "1" ] && /opt/report.sh, with the % escaped because it is special in a crontab.

Why is my AWS EventBridge cron expression not valid?

Two causes account for nearly all "Parameter ScheduleExpression is not valid" errors: EventBridge cron has six fields (minutes, hours, day-of-month, month, day-of-week, year, and no seconds), so a five-field crontab line is one field short, and it refuses a value in both day fields, so one of day-of-month and day-of-week must be a ?. The daily-at-03:00 shape is cron(0 3 * * ? *). While porting, renumber weekdays too: EventBridge counts 1 as Sunday through 7 as Saturday. For plain intervals, rate(15 minutes) avoids cron syntax entirely.

Why does my GitHub Actions scheduled workflow not run at the exact cron time?

GitHub serves schedule events from a shared queue and documents that they can be delayed during periods of high load, naming the start of every hour as the busiest slot; delays of several minutes are normal and runs can be dropped entirely. The floor is one run per 5 minutes, schedules are evaluated in UTC unless the trigger sets an IANA timezone, and in public repositories scheduled workflows are disabled automatically after 60 days without repository activity. Scheduling at an odd minute like 23 instead of 0 avoids the worst of the queue.

How often can Vercel cron jobs run on the Hobby plan?

Once per day per cron job, and the invocation lands somewhere within the scheduled hour: 0 8 * * * fires at any point between 08:00:00 and 08:59:59, because Vercel spreads Hobby traffic across the hour. Schedules more frequent than daily are rejected at deployment. The Pro plan lifts both limits, with per-minute schedules and minute precision. Two more properties to plan around: expressions are evaluated in UTC only, and cron jobs run against the production deployment, so a preview branch never triggers them.

What does H mean in a Jenkins cron schedule?

H is a hash of the job name, evaluated to a stable pseudo-random value within the field range, so H H * * * runs daily at a time that differs per job but never changes for the same job. It exists to break the thundering herd of every job scheduling at minute 0: H/15 * * * * still runs every 15 minutes, just at a per-job offset. Ranges work too, H(0-7) 2 * * * picks a fixed minute in the first eight. Python users get the same behaviour from croniter, which accepts H together with a hash_id argument.

How do I convert a cron expression to a systemd timer?

Translate the schedule into an OnCalendar= line, which reads weekday, then date, then time: 0 3 * * * becomes *-*-* 03:00:00, */15 * * * * becomes *:0/15, and 0 9 * * 1-5 becomes Mon..Fri 09:00. Verify with systemd-analyze calendar "Mon..Fri 09:00", which prints the next elapse times before anything is installed. Timers then add what cron lacks: Persistent=true runs a schedule that was missed while the machine was off, and RandomizedDelaySec spreads simultaneous jobs apart.