Five numbers and some asterisks decide when your backup runs, when your invoices go out, and when the report nobody reads lands in an inbox at four in the morning. Cron syntax is small enough to learn in ten minutes and strange enough that people who have used it for years still get it wrong. This guide reads an expression field by field, works through a real one, and then covers the three things that actually cause the bugs. The Cron Expression Generator will translate any expression as you type and show you the next run times.
The five fields
A crontab line starts with five fields, separated by spaces, always in this order:
- Minute - 0 to 59
- Hour - 0 to 23, on a 24-hour clock
- Day of the month - 1 to 31
- Month - 1 to 12, or JAN to DEC
- Day of the week - 0 to 7, or SUN to SAT, where both 0 and 7 mean Sunday
Four symbols fill them in. A * means every value the field allows. A hyphen makes a range, so 9-17 is every hour from nine in the morning to five in the afternoon. A comma makes a list: 0,30 is on the hour and on the half hour. And a slash makes a step, so */15 in the minute field is 0, 15, 30 and 45.
That is the whole language. Everything else is combinations of those four.
Reading one, left to right
Take 0 9 * * 1-5. Minute 0, hour 9, any day of the month, any month, days one to five of the week. Monday is 1, so that is nine in the morning on weekdays - the single most common cron line in existence.
Now a busier one: */15 9-17 * * 1-5. Minute steps of 15, hours nine through seventeen, weekdays. Every fifteen minutes during the working day, which is 36 runs a day and 180 a week - worth multiplying out before you deploy something that sends an email. Writing one is the same process backwards, and the Cron Expression Generator has a Build tab that assembles the fields from dropdowns.
The trap: day-of-month and day-of-week are OR, not AND
This is the one that bites everybody, and it is not a quirk of any particular implementation - it is in the crontab manual. If both the day-of-month field and the day-of-week field are restricted, meaning neither of them is a *, cron runs the job when EITHER matches. Not both.
The manual's own example is 30 4 1,15 * 5. Read it the natural way and you get "half past four on the 1st and the 15th, if that day is a Friday" - a couple of times a year. What it actually means is half past four on the 1st and the 15th of every month, plus every Friday. In September 2026 that fires on the 1st, the 4th, the 11th, the 15th, the 18th and the 25th: six times, not zero.
There is a further wrinkle. Vixie cron, the crontab on most Linux systems and on macOS, decides whether a field is "restricted" by looking at whether its text starts with a *. So */2 counts as unrestricted and 1-31 does not, even though the two cover almost the same days: change the character and you change the logic. The generator flags whenever this rule is in play. If you genuinely want "the 1st, but only if it is a Monday", cron cannot say it - run on every 1st and let the script check the weekday and exit.
The trap: a step does not wrap
0 */7 * * * looks like "every seven hours". It is not. A step counts from the start of the field and stops at the end, so this gives hours 0, 7, 14 and 21 - and then the day rolls over and the next run is at 0 again, three hours later instead of seven. Three even gaps and one short one, every day.
The same thinking sinks "every 90 minutes". Write */90 in the minute field and there is nothing to match past 59, so you get minute 0 and nothing else: an hourly job in disguise. Cron has no memory of when it last ran, so any interval that does not divide evenly into an hour or a day has to be spelled out. Every 90 minutes is two lines:
- 0 */3 * * * - midnight, 03:00, 06:00 and so on
- 30 1-22/3 * * * - 01:30, 04:30, 07:30 and so on
Between them those cover 00:00, 01:30, 03:00, 04:30 and the rest of the sixteen slots in a day. Steps that divide cleanly - */5, */15, */30 in minutes, */2, */3, */4, */6, */12 in hours - are safe for exactly that reason.
The trap: whose five fields?
Cron syntax has dialects, and pasting one into another is a quiet way to schedule the wrong thing. Standard crontab takes five fields. node-cron, Spring's @Scheduled and many other libraries put a seconds field in front, making six. Java's Quartz - and AWS EventBridge, which copies its shape - takes six or seven, with an optional year on the end.
Two differences matter more than the field count. Quartz requires a ? in exactly one of the two day fields, because it refuses to schedule on day-of-month and day-of-week at once; that is how it sidesteps the OR problem above. And Quartz numbers the week from 1 = Sunday, so 2 is Monday and 6 is Friday - one higher than a crontab all the way along. The same digit in the same position means a different day. Set the flavour in the Cron Expression Generator before you read anything back, because it changes the answer.
Quartz also says things plain cron cannot. L in day-of-month is the last day of the month, LW the last weekday, L-3 three days before the end; 15W is the weekday nearest the 15th without crossing into another month; 6L is the last Friday and 6#3 the third Friday. Month-end billing is the usual reason people find these.
Shorthands, and the clocks
Vixie cron accepts a few nicknames: @yearly, @monthly, @weekly, @daily, @hourly and @reboot. The first five are plain aliases - @daily is exactly 0 0 * * *. @reboot is different in kind: no schedule at all, it runs once when cron starts, and most other schedulers, including systemd timers and Kubernetes CronJob, do not accept it.
Cron reads the machine's clock, which is very often not yours - a container is usually on UTC while you are not. That matters twice a year. The crontab manual says a job skipped by a forward clock change runs soon after the change, so a 02:30 job runs at 03:00 that night; going the other way, a job set for a particular time is not re-run during the repeated hour, while jobs with a wildcard in the hour or minute just follow the new wall clock. The tool shows next runs in whichever zone you pick and flags any that land in a skipped or repeated hour. To reconcile those against log timestamps, the Unix Timestamp Converter reads them in both UTC and local time.
A short checklist
- Count the fields, and confirm which dialect you are in. Five is a crontab; six has seconds in front, or is Quartz.
- If both day fields are restricted, remember it is OR - and check the run list rather than the sentence.
- Check that any step divides evenly into its field, or accept a short gap at the rollover.
- Multiply out how often it really fires before it sends anything to a person.
- Confirm the server's time zone, not yours.
All of this runs in your browser on the Cron Expression Generator - the expression and any job names in it are never uploaded. For the other end of a scheduled job, JSON Formatter and String Escaper handle the payloads it produces.
Frequently asked questions
- What is the difference between */5 and 0-59/5?
- In how the days are worked out, nothing: both give minutes 0, 5, 10 and so on. In one specific case they behave differently, and it is a real one. Vixie cron decides whether the day-of-month and day-of-week fields are "restricted" by checking whether the text starts with a *, and that decides whether the two are combined with OR or with AND. So in the day-of-month field, */1 counts as a wildcard and gives you AND, while 1-31 is treated as restricted and gives you OR, even though the two match every day of the month. In the minute and hour fields there is no difference at all, and */5 is the clearer thing to write.
- Can cron run a job on the last day of the month?
- Not in plain crontab syntax, which has no way to say "last" - the month is 28, 29, 30 or 31 days long and the expression cannot ask. Writing 0 0 31 * * skips February, April, June, September and November entirely. There are two honest fixes. If you are on Quartz, Spring or AWS EventBridge, use L in the day-of-month field. On an ordinary crontab, schedule the job every day and have the script exit immediately unless tomorrow's date is the 1st, which is one line at the top and works in every month length. Scheduling 28-31 and hoping is the version that fires four times in March.
- Why did my cron job not run at all?
- In rough order of likelihood: the expression matched a date that does not exist, such as 0 0 30 2 * on 30 February, which is valid syntax and will never fire; the server is in a different time zone than you assumed, so it ran, just not when you were watching; the crontab has no trailing newline on the last line, which some cron implementations silently ignore; or the job ran and failed, because cron uses a minimal PATH and a bare command that works in your shell may not resolve. Paste the expression into the generator first - if the next-run list is empty or the dates look wrong, the problem is the schedule, and if the dates look right the problem is the environment.