Cron Expression Cheat Sheet

Cron syntax looks cryptic until you've memorized it, then you forget it again the moment you switch jobs. This is the reference: every field, both major dialects, and the mistakes that pass validation but run at the wrong time.

reference guide Hangfire Quartz.NET

// the two dialects

│

Almost every scheduler you'll meet uses one of two cron dialects. Standard cron (what Hangfire, Linux crontab, and most CI systems use) has 5 fields: minute, hour, day-of-month, month, day-of-week. Quartz.NET adds a leading seconds field and uses ? instead of * in whichever of day-of-month/day-of-week isn't in use — 6 fields total (a 7th, optional year field also exists but is rarely set).

field order
Standard / Hangfire (5 fields)minute hour day-of-month month day-of-week
Quartz.NET (6 fields)second minute hour day-of-month month day-of-week
Hangfire / standard cron — 5 fieldsminhourdommonthdowQuartz.NET — 6 fields (seconds prepended)secminhourdommonthdowthe extra fieldDrop a 5-field string into a 6-field scheduler (or vice versa) —every field after the shift is read as the wrong one.
Quartz.NET's six fields are Hangfire's five fields shifted right by the prepended seconds field — the same position means a different field depending on the dialect.

// special characters

syntax
*every value in this field
,a list — MON,WED,FRI
-a range — 9-17 means 9 through 17
/a step — */15 means every 15 units, starting at 0
?"no specific value" — Quartz.NET only, used on whichever of day-of-month/day-of-week you're not constraining (exactly one of the two must be ? — leaving both as * is a parse error, not a wildcard)
L"last" — Quartz.NET only; last day of the month, or last given weekday
#"nth weekday of the month" — Quartz.NET only; 6#3 = third Friday

// combining ranges and steps

│

The characters above compose — a step can apply to a range instead of the whole field, which is where most of cron's real expressive power (and most misreadings) live.

examples
10-30/5every 5 units, but only between 10 and 30 — fires at 10, 15, 20, 25, 30, not before or after
MON-FRIa named range — equivalent to 1-5 in most implementations, but check yours accepts names
0,30a list of two exact values — fires at :00 and :30, not a range
1-5,10-15multiple ranges in one field, comma-separated

The step always starts counting from the beginning of whatever range it's attached to — 10-30/5 starts at 10, not at 0 clipped to the range. This is the same rule as a bare */5 starting at the field's zero point, just applied to a sub-range instead of the whole field.

// common schedules, both dialects

examples
Every minute* * * * * / 0 * * * * ?
Every 15 minutes*/15 * * * * / 0 */15 * * * ?
Every hour, on the hour0 * * * * / 0 0 * * * ?
Daily at 2:30 AM30 2 * * * / 0 30 2 * * ?
Every weekday at 9 AM0 9 * * 1-5 / 0 0 9 ? * 2-6
Every Monday at 6 AM0 6 * * 1 / 0 0 6 ? * 2
First day of every month, midnight0 0 1 * * / 0 0 0 1 * ?
Every 6 hours0 */6 * * * / 0 0 */6 * * ?

// mistakes that pass validation but run wrong

Setting both day-of-month and day-of-week to specific values doesn't mean "AND" — in standard cron it means "OR" (either condition triggers it), which almost never matches what you intended. If you want a specific weekday, leave day-of-month as *.
Day-of-week numbering differs by dialect: standard cron is 0–6 with 0 = Sunday. Quartz.NET is 1–7 with 1 = Sunday. The same digit means a different day depending which dialect you're reading — always check which one you're in before trusting a number. Hangfire's own Cronos engine adds a third wrinkle (0 and 7 both mean Sunday) — the full breakdown covers how a job ends up running on the wrong day despite the expression validating fine.
Cron expressions run in whatever timezone the scheduler process is configured for — not necessarily UTC, and not necessarily the server's local time if the app sets one explicitly. A schedule that looks right can drift by hours after a timezone or daylight-saving change if this isn't pinned down.
A step value like */5 always starts counting from the field's zero point, not from "5 units from now." */5 on minutes fires at :00, :05, :10 — never at :02, :07, :12, no matter what minute you deployed at.

// pinning down the timezone

│

A cron string has no timezone of its own — it's just field values, interpreted against whatever clock the scheduler evaluates it with. That clock is a separate, easy-to-miss setting, and getting it wrong doesn't throw an error; the job just quietly runs at the wrong wall-clock time.

where the timezone actually gets set
HangfireRecurringJob.AddOrUpdate(..., cron, new RecurringJobOptions { TimeZone = TimeZoneInfo.FindSystemTimeZoneById(...) }) — omit it and Hangfire schedules in UTC
Quartz.NETCronScheduleBuilder.CronSchedule(expr).InTimeZone(TimeZoneInfo...) — omit it and Quartz uses TimeZoneInfo.Local, i.e. whatever the host machine is set to

That default mismatch — Hangfire falling back to UTC, Quartz falling back to the host's local time — is itself a common source of "this ran three, four, or five hours off" bugs when a job moves between a developer's machine, a container, and a cloud region without the timezone ever being set explicitly on either side. Pin it explicitly in code rather than relying on either default, and remember that a timezone with daylight saving (unlike UTC) means the same cron expression covers a different number of real hours on the two days each year the clock shifts — see Unix Timestamp & Epoch Time Explained for exactly why UTC/epoch sidesteps this and local wall-clock time can't.

// Cron helper methods vs raw expressions (Hangfire)

│

Hangfire's Cron class ships a set of static helpers that return a cron string for the common cases, so most recurring jobs never need a hand-written expression at all.

Hangfire.Cron helpers
Cron.Minutely()→ * * * * *
Cron.Hourly()→ 0 * * * *
Cron.Daily(2, 30)→ 30 2 * * *
Cron.Weekly(DayOfWeek.Monday, 6)→ 0 6 * * 1
Cron.Monthly(1)→ 0 0 1 * *

The helpers exist because a call like Cron.Daily(2, 30) is self-documenting at the call site in a way "30 2 * * *" never is, and it can't be typo'd into a subtly different valid expression. Reach for a raw string only once a schedule needs something the helpers don't cover — a step value, a multi-day list, or anything Quartz-specific like L/W/# — and paste it into the Cron Builder first to confirm it means what you think before it ships.

// frequently asked questions

What's the difference between Hangfire and Quartz.NET cron syntax?

Hangfire and standard cron use 5 fields: minute, hour, day-of-month, month, day-of-week. Quartz.NET adds a leading seconds field and uses ? instead of * in whichever of day-of-month/day-of-week isn't in use — 6 fields total. Quartz.NET's six fields are Hangfire's five shifted one position right by the prepended seconds field, so dropping a 5-field string into a 6-field scheduler (or vice versa) reads every field after the shift as the wrong one.

What does the ? character mean in a cron expression?

"No specific value" — Quartz.NET only. It's used on whichever of day-of-month/day-of-week you're not constraining. Exactly one of the two must be ?; leaving both as * is a parse error, not a wildcard.

Why doesn't setting both day-of-month and day-of-week work as AND?

In standard cron, specific values in both fields mean OR — either condition triggers the job — which almost never matches what you intended. If you want a specific weekday, leave day-of-month as *.

What timezone do cron expressions run in?

Whatever timezone the scheduler process is configured for — not necessarily UTC, and not necessarily the server's local time. Hangfire falls back to UTC if you don't set one explicitly; Quartz.NET falls back to TimeZoneInfo.Local, the host machine's timezone. That default mismatch is a common source of jobs running hours off after moving between a developer's machine, a container, and a cloud region.

Does 1 mean Monday or Sunday in a cron day-of-week field?

It depends on the dialect. Standard cron is 0–6 with 0 = Sunday. Quartz.NET is 1–7 with 1 = Sunday. Hangfire's Cronos engine accepts 0–7 where both 0 and 7 mean Sunday. The same digit can mean a different day depending which one you're reading — see the full breakdown of how this causes a job to silently run on the wrong day.

// try it yourself

│

Build a schedule field-by-field, or paste an existing expression to get a plain-English explanation and its next five run times, with the Cron Builder & Explainer.