Cron expressions, decoded: the five fields, the traps, and how to test a schedule
Five numbers and some asterisks look like they should be readable, and then you meet 0 0 13 * 5 and discover it does not mean Friday the 13th, or you deploy a backup that quietly stops running for an hour every autumn. Cron syntax is small, old and full of decisions made in the 1970s that nobody can change now. This guide covers the five fields and their operators, the day-of-month/day-of-week rule that surprises nearly everyone, the non-standard variants you will meet in Quartz, Jenkins, Kubernetes and AWS, what time zones and daylight saving do to a schedule, and the things cron deliberately does not do for you. Every example was run through the same parser that powers the cron expression parser on this site, so the run times quoted are what you will see if you paste them in yourself.
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 (0 = Sunday; 7 also accepted; or SUN-SAT)
│ │ │ │ │
* * * * *Each field accepts four operators, and everything else is built from them:
| Operator | Meaning | Example |
|---|---|---|
* | Every value in the field | * * * * * — every minute |
, | A list | 0 8,12,18 * * * — 08:00, 12:00 and 18:00 |
- | An inclusive range | 0 9-17 * * * — every hour from 09:00 to 17:00 |
/ | A step through a range | */15 * * * * — minute 0, 15, 30, 45 |
Steps combine with ranges, which is where cron gets expressive: */5 9-17 * * 1-5 means every five minutes between 09:00 and 17:59, Monday to Friday. Note the 17:59 — an hour range of 9-17includes the whole of the 17th hour, so the last run of the day is 17:55, not 17:00. Cronstrue phrases this as “between 09:00 AM and 05:59 PM” for exactly that reason.
One arithmetic trap: a step only makes sense as an even division of its range. */7 on the minutes field fires at 0, 7, 14, 21, 28, 35, 42, 49, 56 — and then again at 0, four minutes later. If you want an even cadence, use a divisor of 60.
The OR rule nobody expects
This is the single most important thing to know about cron. When both the day-of-month and the day-of-week fields are restricted (neither is *), the job runs when either matches — not when both do.
So 0 0 13 * 5is not Friday the 13th. It is “midnight on the 13th of the month, and also midnight every Friday”. Pasting it into the parser from late March 2026 gives 27 March, 3 April, 10 April, 13 April — a Monday — 17 April, 24 April, 1 May. Roughly nine runs a month, not the one or two a year you had in mind.
The same applies to 0 0 1 * 1: the 1st of every month plus every Monday. If one of the two fields is *, there is no ambiguity and the other simply applies — which is why almost every schedule you write should leave one of them as *. When you genuinely need an AND, schedule the looser condition and test the tighter one in the script:
# Friday the 13th, done properly
0 0 13 * * [ "$(date +\%u)" = "5" ] && /usr/local/bin/report(The backslashes matter: in a crontab, an unescaped % is turned into a newline, which truncates the command. It is the second most common crontab bug after the OR rule.)
The variants you will actually meet
“Cron syntax” is a family, not a standard. The differences are small and they will bite you when you move a schedule between systems:
- Six fields with seconds. Quartz, Spring, many Node and Go libraries put seconds first, so
0 30 2 * * *is 02:30:00 daily, not something at minute 30 of hour 2 of every day-of-month. The parser here accepts the six-field form; a five-field expression pasted into a six-field scheduler shifts everything by one field, which is the classic “it ran 60 times an hour” incident. - Shortcuts.
@yearly,@monthly,@weekly,@daily,@hourlyand@reboot. All except@rebootmap to ordinary expressions, and the parser here understands them. - Names.
MON-FRIandJANwork in most implementations, including this one, and read better than numbers. Note that7and0both mean Sunday. - Quartz extras.
L(last),W(nearest weekday),#(nth weekday of the month, so6#3is the third Friday), and?for “no specific value” in one of the two day fields — which is how Quartz sidesteps the OR problem. These are not standard Unix cron and will be rejected elsewhere. - AWS EventBridge uses six fields ending in a year, requires
?in exactly one of the day fields, and always evaluates in UTC unless you configure otherwise. - Jenkins adds
H, which hashes the job name to spread load —H/5 * * * *means “every five minutes, starting at some fixed minute derived from the job name”. It is a genuinely good idea and it is not portable; a standard parser rejects it. - systemd timers abandon cron syntax entirely for
OnCalendar=, which is more precise and handles missed runs (Persistent=true) in a way cron never has.
Time zones, and the two nights a year that break jobs
A cron expression carries no time zone. Classic crondevaluates it in the system zone, which on servers and in containers is usually UTC — so a schedule someone wrote as “09:00, before the team starts” runs at 09:00 UTC, which is 04:00 in New York and 14:30 in Delhi. Where to set it depends on the platform: CRON_TZ=Europe/Berlin at the top of a crontab, spec.timeZone on a Kubernetes CronJob (a stable field since 1.27), a dropdown in most managed schedulers.
Daylight saving then adds two awkward nights a year, and the behaviour is genuinely implementation-specific:
- Spring forward: an hour of local time does not exist. A job scheduled at 02:30 has no 02:30 to run at. Implementations variously skip it, run it once at the shifted instant, or run it immediately after the jump.
- Autumn back:an hour repeats. A job scheduled at 01:30 has two 01:30s. Some implementations run it twice; the parser behind this site’s tool advances to the first occurrence and then to the next day, so it runs once.
The reliable answer is the boring one: schedule anything important in UTC, or outside 00:00–03:00 local, and make the job idempotent so a double run is harmless. Hourly and sub-hourly schedules are unaffected either way; it is the once-a-day job that silently misses a night.
What cron does not do
Most cron incidents are not syntax errors. They are expectations the scheduler never promised to meet:
- No retries. A failed run is just a process that exited. Traditional cron mails the output to the user; in a container that mail goes nowhere. If a failure matters, the script has to report it.
- No overlap protection. If the 5-minute job takes 7 minutes, you will have two copies running. Wrap it in
flockor an equivalent lock. - No catch-up. A machine that was asleep or down at the scheduled time simply misses the run.
anacron, systemd’sPersistent=trueor a job queue exist for this; cron does not. - Almost no environment. Cron runs with a minimal
PATHand none of your shell profile, which is why a script that works in your terminal fails at 03:00. Use absolute paths for every binary, set the variables you need at the top of the crontab, and redirect output somewhere you will read it. - No visibility. There is no dashboard. If the job matters, have it report success to something that alerts on silence.
Test the schedule before you deploy it
Reading a cron expression back to yourself is how mistakes survive review; seeing the next five run times is how they get caught. The cron parser here does both: type or paste an expression and it shows a plain-English reading of it plus the next five firings, computed in your browser and in your own time zone. That local-time detail is deliberate — it is the difference between “every 5 minutes on weekdays” and knowing the first one lands at 09:00 tomorrow rather than tonight. Preset chips cover the schedules people want most often, and an invalid expression gets the parser’s own message: Constraint error, got value 60 expected range 0-59 for a minute field of 60, Invalid explicit day of month definition for 31 April.
The check that catches the most bugs takes ten seconds: paste the expression, read the English sentence, and confirm the five dates are the days you meant. Nine runs a month where you expected one is obvious the moment you can see them. And if the job’s timing is expressed in epoch seconds anywhere downstream, the timestamp converter will turn those into readable dates.
Do this
- Leave one of day-of-month and day-of-week as
*. If both are set, cron ORs them. - Paste the expression into a parser and read the next five run times before deploying it.
- Count the fields: five means minutes first, six usually means seconds first. Moving a schedule between the two shifts everything.
- State the time zone explicitly, and keep once-a-day jobs out of 00:00–03:00 local — or schedule them in UTC.
- Use step values that divide their range evenly (
*/5,*/15), not*/7. - Escape
%in crontab commands, use absolute paths, and wrap anything that could overlap in a lock. - Make the job idempotent and have it report success somewhere that alerts on silence.
Frequently asked questions
Does 0 0 13 * 5 run on Friday the 13th?
No — it runs on the 13th of every month AND on every Friday. When both the day-of-month and day-of-week fields are restricted, cron ORs them instead of ANDing them. Verified against the parser this site uses: from late March 2026 the schedule fires on 27 March, 3 April, 10 April, 13 April (a Monday) and 17 April. To get a genuine Friday-the-13th job you have to check the day inside the script.
What time zone does a cron job run in?
Classic Unix cron uses the system time zone of the machine running crond, which on a server is very often UTC even though you wrote the schedule thinking in local time. You can set CRON_TZ at the top of a crontab; Kubernetes CronJobs have a spec.timeZone field; most managed schedulers let you pick. If nothing documents it, assume UTC and verify.
What happens to a daily job during a daylight-saving change?
It depends on the implementation, which is why schedules between midnight and 03:00 local are worth avoiding. In spring an hour does not exist, so a job scheduled inside it either runs at a shifted time or is skipped; in autumn an hour repeats, so a job can run twice or once depending on the scheduler. Anything hourly or more frequent is unaffected. If a job must run exactly once a day, schedule it in UTC.
Why does */7 not do what I expect at the end of the hour?
A step applies within the field range, restarting each hour: */7 on minutes gives 0, 7, 14, 21, 28, 35, 42, 49, 56 — and then the next run is at minute 0, only four minutes later. Steps that do not divide 60 evenly always produce a short interval at the boundary. Use a divisor of 60 (2, 3, 4, 5, 6, 10, 12, 15, 20, 30) if you need an even spacing.
Does cron retry a job that fails?
No. Cron starts a process at a time and has no idea whether it succeeded; a non-zero exit status just produces mail on a traditional system and nothing at all in most containers. It also will not skip a run because the previous one is still going, so a slow job can overlap itself. Retries, locking and alerting are your script’s job, not the scheduler’s.
Tools used in this guide
Every one of these runs in your browser — the files you work on never leave your device.