Developer Tutorials • 9 min read

Cron Expression Cheat Sheet: Fields, Examples and Traps

A crontab line is five columns of numbers and nobody memorises them. Here is the whole syntax read one column at a time, the expressions that cover almost every real job, and the six places where what you wrote is not what the server runs.

Read the Five Columns Left to Right

Every standard crontab schedule is five space-separated fields in a fixed order, and the column order is the source of most mistakes: minute, hour, day of month, month, day of week. The first number you write is never the hour. The line `0 9 * * *` means 09:00 every morning because the minute field takes the 0 and the hour field takes the 9.

Each field accepts one of five shapes: a single value, a comma-separated list, an inclusive range written `start-end`, a step written `*/n`, or an asterisk meaning every value the field allows. Any expression where all five columns contain one of those shapes is a valid schedule, which is why a broken cron line rarely produces an error, it just quietly never fires.

The ranges are the ones you would guess off a clock, with one exception worth memorising. Day of week runs 0 to 6 and 0 is Sunday, so a weekly job scheduled for Sunday is `0 0 * * 0`. Most cron implementations also accept 7 for Sunday, but the number you should copy into a line other people will read is 0.

The five crontab fields, their legal ranges and what an asterisk means in each
ColumnLegal valuesAn asterisk here meansWorked example
1. Minute0-59every minute of the matching hour`30` fires at half past
2. Hour0-23every hour of the matching day`9` fires at 09:00
3. Day of month1-31every day of the matching month`1` fires on the first
4. Month1-12every month of the year`1,4,7,10` fires quarterly
5. Day of week0-6, Sunday is 0every weekday and weekend`1-5` fires Monday to Friday

Note: The generator on this site requires exactly five columns. A six-field expression with a leading seconds column (Quartz, Spring and the Node cron library all use one), a seven-field expression, and shorthand strings like `@daily` or `@reboot` come back as invalid format.

Fifteen Expressions That Cover Almost Every Real Job

These are the schedules worth copying, all of them parsed and previewed by the generator on this site rather than quoted from memory. The first ten are the presets built into the tool; the rest are the combinations people reach for once the basics are in place.

Read them as sentences, not as symbols. `*/15 * * * *` is every fifteen minutes, `0 9 * * 1` is nine in the morning on Monday, and `0 0 1 1,4,7,10 *` is midnight at the start of January, April, July and October.

Working crontab expressions with the plain English each one produces
ExpressionPlain EnglishFires at
`* * * * *`every minute60 times a day
`*/5 * * * *`every five minutes:00, :05, :10 and so on
`*/15 * * * *`every fifteen minutes:00, :15, :30, :45
`0 * * * *`on the hourevery hour at :00
`0 0 * * *`daily at midnight00:00 every night
`0 9 * * *`daily at nine09:00 every morning
`0 9 * * 1-5`weekdays at nineMon to Fri, 09:00
`0 9 * * 1`every Monday at nineone run a week
`0 0 * * 0`Sunday midnightstart of the week
`0 0 1 * *`first of the month00:00 on the 1st
`0 0 1 1,4,7,10 *`quarterly00:00 on Jan, Apr, Jul, Oct 1st
`30 2 * * *`daily at 02:30the usual backup window
`0 */6 * * *`every six hours00:00, 06:00, 12:00, 18:00
`*/10 9-17 * * 1-5`every ten minutes in office hours09:00 to 17:50, Mon to Fri
`15 3 * * 6`03:15 on Saturdayone quiet weekly slot

How Steps, Lists and Ranges Combine

A step is a divisibility test, which is the easiest way to predict what `*/n` will do. `*/15` in the minute field means minutes divisible by 15, so :00, :15, :30 and :45. `*/6` in the hour field means hours divisible by 6, so 00, 06, 12 and 18. Put a fixed minute in front of an hourly step and you get a schedule with an offset: `30 */6 * * *` fires at 00:30, 06:30, 12:30 and 18:30.

Lists are unions and ranges are inclusive at both ends, and the two can appear in different columns of the same line. `0 0 1,15 * 1-5` says: minute 0, hour 0, the 1st or the 15th, any month, and only when the day of week is Monday through Friday.

When a schedule cannot be written as one field, write two lines instead of getting clever. Every ninety minutes is a good example, because ninety does not divide the hour and no single field can carry it. The day works out to sixteen slots that alternate between the hour and the half hour:

  • `0 0,3,6,9,12,15,18,21 * * *` fires on the exact hours of that list.
  • `30 1,4,7,10,13,16,19,22 * * *` fires on the half hour of the hours in between.
  • Those two lines together give 00:00, 01:30, 03:00, 04:30 and onward in ninety-minute steps, and they wrap the day exactly: sixteen runs, and 22:30 plus ninety minutes is midnight.
  • Anything you cannot express cleanly is usually a sign the job wants a systemd timer or a queue with a delay, not a crontab.

Note: One shape real cron accepts and this preview does not: a range with a step, written `10-30/5`. The preview reads it as a range whose upper bound is the text `30/5`, matches nothing, and shows an empty upcoming list without warning. Write the list out instead.

Where a Preview and a Real Server Disagree

This is the section that saves the most time, because every item here produces a schedule that looks correct on screen and behaves differently in production.

First, the two day columns. In Vixie-style cron, which is what most Linux distributions ship, day of month and day of week are combined with OR whenever both are restricted: `0 0 13 * 5` fires on the 13th of every month and also on every Friday. The generator here applies the ordinary AND, and its plain-English line says on day 13 of the month and on Fri. If a schedule mixes both columns, read the man page for the cron you actually run before trusting either answer.

Second, the clock. The upcoming-run preview walks the clock in your browser, while crontab fires on the clock of the machine running cron. A server on UTC that you think of as local time is five or more hours away from your desk, so `0 9 * * *` can land at 04:00 your time. Check the server with `timedatectl` and schedule against that, not against your tab.

Third, daylight saving. On a spring-forward night the local times between 02:00 and 03:00 do not exist, so a job at 02:30 is skipped; on the fall-back night they occur twice and many crons fire once, some fire twice, and the answer depends on the implementation and on whether the daemon was restarted. A minute-by-minute preview cannot show either outcome.

Fourth, Sunday written as 7. Real cron accepts 0 and 7 for Sunday; the preview matches only 0, so a line written with 7 shows nothing coming up. Use 0 and the line is portable and previewable.

Fifth, month and weekday names. crontab(5) allows JAN, FEB and MON, TUE in the text fields, which is handy in a hand-edited file and unreadable by this generator, which expects numbers. Keep numbers in anything another person has to maintain.

Sixth, the preview window. The search steps one minute at a time and stops after 50,000 iterations, which is about 34 days ahead. A yearly schedule is still a correct schedule, but its next run may sit outside the window and simply not appear in the list.

From Expression to a Line You Can Paste

The five columns are only half of a crontab entry. The rest is the command, and the command is where jobs actually fail. The generator appends whatever you type in the command box to the verified expression, then offers three output modes, so use it to produce the whole line rather than the schedule alone.

  1. Open your own crontab — `crontab -e` edits the crontab for the user running the command, one job per line. It is not the same file as `/etc/crontab`, and files in `/etc/cron.d` carry an extra user column between the time fields and the command, which is the single most common reason a pasted line does nothing.
  2. Use absolute paths everywhere — cron runs with a minimal environment, so a bare `python3 script.py` depends on a PATH you do not control and a working directory that is usually the home directory. The generator's default command, `/usr/bin/python3 /app/tasks/backup.py`, is written the way the line has to be written: interpreter by path, script by path.
  3. Decide what happens to the output — The three modes are append, discard and standard. Append puts `>> /var/log/cron-job.log 2>&1` on the line so stdout and stderr both land in a file you can read. Discard puts `> /dev/null 2>&1`, which is right for noisy jobs you do not want to keep. Standard leaves the line bare, and cron then mails whatever the job printed to the owner of the crontab, which only helps if mail is actually configured on that box.
  4. Say which shell when you need one — A job that depends on your login profile, version manager or aliases needs an explicit shell: `/bin/bash -lc 'your command'`. Without it you get a plain `sh` with almost nothing loaded.
  5. Confirm the first run before you trust the schedule — Watch for a timestamped line in the log, check the daemon's own record with `grep CRON /var/log/syslog` or `journalctl -t cron`, and have the job touch a heartbeat file a monitor can age out. A silent success and a silent failure look identical from outside.

Note: The previewed times and the copied line are produced in the page from your own browser state. Nothing about your schedule, your paths or your command leaves it, which matters more for a crontab than for most tools: those lines describe where your production jobs live.

Schedules That Look Right and Are Not

Five more ways a correct-looking line behaves differently from the intention behind it. None of these are errors, which is exactly why they survive code review.

  • `0 0 31 * *` is not monthly. It fires on the 31st, and only seven months have one, so you get seven runs a year: January, March, May, July, August, October and December.
  • `0 0 30 2 *` never fires at all. February 30th does not exist, and cron does not complain.
  • `*/5 * * * *` starts a new copy every five minutes whether or not the previous copy finished. A backup that sometimes takes eight minutes will overlap; guard it with `flock -n /tmp/backup.lock your-command`.
  • `0 9 * * 1-5` knows nothing about public holidays, your office calendar, or the fact that the person who reads the report is on leave.
  • A minute of 60 or an hour of 24 is out of range and will not be caught by a five-column check, because the column count is right and only the value is wrong.

Questions About Cron Syntax and Scheduling

What does 0 0 * * * mean in a crontab?

It fires at 00:00 every day. The minute field is 0, the hour field is 0, and all three day and month columns are asterisks, so nothing restricts which days qualify. Midnight in which timezone is decided by the server running cron, not by the line.

What does 0 9 * * 1 do?

09:00 every Monday. The final column is day of week, where 0 is Sunday and 1 is Monday, so this is the shortest correct way to write a weekly weekday morning job. Real cron also accepts MON in that field, but numbers are what this generator reads.

How do I run a job every five minutes?

`*/5 * * * *`. The minute field is a step, so it matches every minute divisible by 5: :00, :05, :10 through :55. If the job sometimes takes longer than five minutes, add a lock, because cron will happily start a second copy while the first is still running.

Can cron run something every 90 minutes?

Not in one line, because the minute field resets when the hour rolls over and 90 minutes does not divide an hour. Sixteen runs a day do land on exact hour and half-hour boundaries, so two readable lines get you there: one with the minute fixed at 0 over the hours you want, and one with the minute fixed at 30 over the hours that complete the cadence.

Does crontab support seconds?

Standard Unix and Linux crontab has no seconds field; its shortest interval is one minute. Quartz, Spring and several job frameworks use a six or seven field format that starts with seconds, and those lines are rejected by a normal crontab. Systems that need sub-minute scheduling generally want a timer or a resident worker rather than cron.

My cron job did not run. Where do I look first?

In this order: is the cron daemon actually running, especially inside a container; does `crontab -l` show the line for the user you think you edited; are the paths absolute and executable by that user; is the server clock the timezone you assumed; and is output going somewhere you can see. A job that runs but discards its errors and a job that never ran look the same from outside.

Where These Numbers Come From

Keep Reading

Three guides that pick up where this one leaves off: