Why one cron expression is not enough
Cron started as five fields in Unix. The .NET libraries all extended it, and not in the same way. Quartz.NET adds seconds and an optional year, and demands a ?. Azure Functions adds seconds. Hangfire (through Cronos) accepts both five and six fields and adds L, W and #. Paste an expression from one into another and it either fails to parse or, worse, parses and runs at different times.
Every rule on this page was checked by running the real libraries: Cronos 0.13, NCrontab 3.4 and Quartz.NET 3.15.
How the three libraries differ
| Cronos (Hangfire) | NCrontab (Azure Functions) | Quartz.NET | |
|---|---|---|---|
| Fields | 5, or 6 with seconds first | 5, or 6 with seconds first (Azure needs 6) | 6 or 7: seconds first, optional year last |
| Sunday | 0 or 7 | 0 | 1 (Saturday is 7) |
? | Same as * | Not allowed | Required in day of month or day of week |
| Both day fields set | Runs when both match | Runs when both match | Not allowed |
L, W, # | Yes, each on its own | No | Yes, each on its own |
Range like 22-2 | Wraps: 22, 23, 0, 1, 2 | Swapped: 2 to 22 | Wraps: 22, 23, 0, 1, 2 |
* in a list | Not allowed | Allowed | Allowed |
| Step of 0 | Error | Treated as 1 | Accepted (avoid it) |
Macros like @daily | Yes | No | No |
Reading a cron expression
Count the fields
Five fields start with minutes. Six start with seconds. Seven is Quartz.NET with a year at the end.
Read each field as a set of allowed values
*is every value,1-5a range,1,15a list,*/15every 15th value starting at the lowest, and5/15every 15th value starting at 5.The job runs when every field matches
At 09:30 on a Monday,
30 9 * * 1-5matches: minute 30, hour 9, any day of month, any month, day of week 1.
Special characters
| Syntax | Meaning | Example |
|---|---|---|
L (day of month) | Last day of the month | 0 0 L * * runs on 31 Jan, 29 Feb in a leap year, 31 Mar |
L-3 | Three days before the last day | 0 0 L-3 * * |
LW | Last weekday (Monday to Friday) of the month | 0 18 LW * * |
15W | Weekday nearest the 15th, without crossing into another month | 0 9 15W * * |
5L (day of week) | Last Friday of the month (Cronos numbering) | 0 17 * * 5L |
5#3 | Third Friday of the month (Cronos numbering) | 0 9 * * 5#3 |
Common mistakes
- Pasting a five-field expression into Azure Functions.
*/5 * * * *fails; the timer trigger needs0 */5 * * * *. - Using Unix day numbers in Quartz.NET.
0 0 9 ? * 1-5is Sunday to Thursday in Quartz. UseMON-FRIor2-6. - Expecting "either day" behaviour.
0 0 1 * MONin Hangfire runs only when the 1st is a Monday, not on every Monday plus every 1st. - Wrapping a range in Azure Functions.
0 0 22-2 * * *runs every hour from 02:00 to 22:00 there. Write0 0 22-23,0-2 * * *. - Forgetting the time zone. Quartz.NET uses the server's local time by default; Hangfire and Azure use UTC.
Common schedules
All ten are below, translated for Hangfire (Cronos), Azure Functions (NCrontab) and Quartz.NET. Every minute, every 5 minutes, every 15 minutes and every hour also have their own page, with next run times and C# code.
| Schedule | Hangfire (Cronos) | Azure Functions (NCrontab) | Quartz.NET |
|---|---|---|---|
| Every minute | * * * * * | 0 * * * * * | 0 * * * * ? |
| Every 2 minutes | */2 * * * * | 0 */2 * * * * | 0 */2 * * * ? |
| Every 5 minutes | */5 * * * * | 0 */5 * * * * | 0 */5 * * * ? |
| Every 10 minutes | */10 * * * * | 0 */10 * * * * | 0 */10 * * * ? |
| Every 15 minutes | */15 * * * * | 0 */15 * * * * | 0 */15 * * * ? |
| Every 30 minutes | */30 * * * * | 0 */30 * * * * | 0 */30 * * * ? |
| Every hour | 0 * * * * | 0 0 * * * * | 0 0 * * * ? |
| Every 2 hours | 0 */2 * * * | 0 0 */2 * * * | 0 0 */2 * * ? |
| Every 6 hours | 0 */6 * * * | 0 0 */6 * * * | 0 0 */6 * * ? |
| Every day | 0 0 * * * | 0 0 0 * * * | 0 0 0 * * ? |
FAQ
Why does Quartz.NET need a question mark?
Quartz.NET does not let you restrict day of month and day of week at the same time. One of the two must be ?, meaning no specific value. 0 0 12 * * ? runs every day at noon; 0 0 12 ? * MON-FRI runs on weekdays.
Is Sunday 0, 1 or 7?
It depends on the library. Cronos (Hangfire) accepts 0 and 7 for Sunday. NCrontab (Azure Functions) accepts only 0. Quartz.NET counts from 1, so Sunday is 1 and Saturday is 7. Day names like SUN and MON work in all three, which avoids the question.
Why does my Azure Functions timer trigger need six fields?
Azure Functions uses NCrontab with seconds enabled, so the first of six fields is seconds. Every five minutes is 0 */5 * * * *, not */5 * * * *.
What time zone do the run times use?
This page computes run times in UTC; switch to "My time zone" to see them converted. Hangfire uses UTC unless you set RecurringJobOptions.TimeZone. Azure Functions uses UTC unless the WEBSITE_TIME_ZONE app setting changes it on plans that support it. Quartz.NET uses the server's local time zone unless you call InTimeZone. Around daylight saving changes, local-time schedules can skip or repeat a run, and the libraries handle that differently.
What happens when both day of month and day of week are set?
In Cronos and NCrontab the job runs only on days that match both, so 0 0 13 * 5 runs on Friday the 13th. Classic Unix cron runs when either matches. Quartz.NET does not allow both, so the expression fails to parse.
Related tools
- Unix timestamp and .NET ticks converter
- Windows and IANA time zone converter
- C# DateTime format tester