Cron has run scheduled work on Unix systems since the 1970s and its syntax has barely changed. It is compact, universally available, and contains exactly one genuinely surprising rule that has silently broken more schedules than every other feature combined. This page covers the syntax completely, gives forty schedules you can paste directly, and explains the traps.
The five fields
minute hour day-of-month month day-of-week, separated by spaces. 0 3 * * * runs at 03:00 every day. Read left to right, smallest unit first.
The field layout
| Field | Range | Names allowed | Notes |
|---|---|---|---|
| Minute | 0–59 | — | The finest resolution cron offers |
| Hour | 0–23 | — | 24-hour clock; midnight is 0 |
| Day of month | 1–31 | — | 31 simply does not fire in short months |
| Month | 1–12 | JAN–DEC | Names are case-insensitive |
| Day of week | 0–7 | SUN–SAT | Both 0 and 7 mean Sunday |
Some schedulers — Quartz, Spring, and several JavaScript libraries — add a sixth field for seconds at the front. If a six-field expression is rejected by system cron, that is why. Check which dialect you are writing for before assuming an expression is portable.
The operators
| Operator | Name | Meaning | Example |
|---|---|---|---|
* | Asterisk | Every value in the field | * * * * * — every minute |
, | Comma | A list of specific values | 0 9,12,17 * * * — 09:00, 12:00, 17:00 |
- | Hyphen | An inclusive range | 0 9-17 * * * — hourly, 09:00 to 17:00 |
/ | Slash | Step through a range | */15 * * * * — every 15 minutes |
Operators combine within a single field, which is where cron gets expressive:
⚠️ The step operator does not mean "every N"
*/N means "every Nth value counting from the start of the range", not "every N units of time". In the minute field the two coincide because minutes begin at 0. In the day-of-month field they do not: */5 fires on the 1st, 6th, 11th, 16th, 21st, 26th and 31st — and then the 1st of the next month, only one day later. The counter resets each month rather than continuing.
The same applies to hours: 0 */7 * * * fires at 00:00, 07:00, 14:00 and 21:00 — a three-hour gap between the last and the first of the next day, not seven.
Special strings
| String | Equivalent | Meaning |
|---|---|---|
@yearly / @annually | 0 0 1 1 * | Midnight, 1 January |
@monthly | 0 0 1 * * | Midnight on the 1st |
@weekly | 0 0 * * 0 | Midnight on Sunday |
@daily / @midnight | 0 0 * * * | Every midnight |
@hourly | 0 * * * * | Top of every hour |
@reboot | — | Once, when cron starts at boot |
@reboot is worth a caution: it fires when the cron daemon starts, which is not the same moment as "the system is ready". Network interfaces, mounted volumes and databases may not be available yet. For anything with real dependencies, a systemd unit with explicit After= ordering is the correct tool.
The rule that breaks schedules
This is the single most important thing on this page.
When both the day-of-month and day-of-week fields are restricted — neither is * — cron matches if either condition is true. It is an OR, not an AND.
Every other field combination is an AND. Only these two fields behave this way, and the behaviour is inherited from the original 1970s implementation — it is specified, documented in the POSIX standard, and completely counter-intuitive.
| Expression | Day-of-month | Day-of-week | Runs on |
|---|---|---|---|
0 0 1 * * | Restricted | * | The 1st only |
0 0 * * 1 | * | Restricted | Mondays only |
0 0 1 * 1 | Restricted | Restricted | The 1st OR any Monday |
0 0 1-7 * 1 | Restricted | Restricted | Days 1–7 OR any Monday |
✅ How to express "first Monday of the month"
Restrict only the day-of-week field, then test the date inside the command. The first Monday is necessarily within the first seven days:
40 ready-to-use schedules
Frequent intervals
| Expression | Runs |
|---|---|
* * * * * | Every minute |
*/2 * * * * | Every 2 minutes |
*/5 * * * * | Every 5 minutes |
*/10 * * * * | Every 10 minutes |
*/15 * * * * | Every 15 minutes |
*/30 * * * * | Every 30 minutes |
0 * * * * | Every hour, on the hour |
15 * * * * | 15 minutes past every hour |
0 */2 * * * | Every 2 hours |
0 */6 * * * | Every 6 hours |
0 */12 * * * | Every 12 hours |
Daily
| Expression | Runs |
|---|---|
0 0 * * * | Every day at midnight |
30 2 * * * | Every day at 02:30 |
0 9 * * * | Every day at 09:00 |
0 9,17 * * * | Twice daily, 09:00 and 17:00 |
0 8-18 * * * | Hourly, 08:00 to 18:00 |
*/30 9-17 * * * | Every 30 min during office hours |
0 23 * * * | Every night at 23:00 |
Weekly
| Expression | Runs |
|---|---|
0 0 * * 0 | Sunday at midnight |
0 9 * * 1 | Monday at 09:00 |
0 9 * * 1-5 | Weekdays at 09:00 |
0 9 * * 6,0 | Weekends at 09:00 |
0 17 * * 5 | Friday at 17:00 |
*/15 * * * 1-5 | Every 15 min, weekdays only |
0 22 * * MON-FRI | Weeknights at 22:00 (named days) |
0 3 * * 2,4 | Tuesday and Thursday at 03:00 |
Monthly and yearly
| Expression | Runs |
|---|---|
0 0 1 * * | 1st of the month, midnight |
0 4 1 * * | 1st of the month at 04:00 |
0 0 15 * * | 15th of the month |
0 0 1,15 * * | 1st and 15th |
0 0 1 */3 * | Quarterly — Jan, Apr, Jul, Oct |
0 0 1 1,7 * | Twice yearly — 1 Jan and 1 Jul |
0 0 1 1 * | 1 January, midnight |
0 0 25 12 * | 25 December |
0 0 1 JAN,APR,JUL,OCT * | Quarterly, named months |
Practical maintenance patterns
| Expression | Typical use |
|---|---|
17 3 * * * | Nightly backup — odd minute avoids the 03:00 stampede |
42 4 * * 0 | Weekly deep clean, Sunday early morning |
0 2 * * 1-5 | Weeknight batch processing |
*/5 * * * * | Health check or queue drain |
0 6 * * * | Daily report before the working day |
💡 Do not schedule on the hour
Almost every default schedule fires at 0 minutes. On a busy machine — or against a shared API — that produces a spike of simultaneous work every hour, and a much larger one at midnight. Offsetting to an arbitrary minute like 17 or 42 costs nothing and spreads the load. This is why well-maintained system crontabs are full of apparently random minute values.
Non-standard extensions
Quartz, Spring, and several cloud schedulers support operators that system cron does not. They are useful, and they are not portable.
| Operator | Meaning | Example |
|---|---|---|
L | Last | 0 0 L * ? — last day of the month |
L (weekday field) | Last given weekday | 0 0 ? * 5L — last Friday |
W | Nearest weekday | 0 0 15W * ? — nearest weekday to the 15th |
# | Nth weekday | 0 0 ? * 2#1 — first Monday |
? | No specific value | Used in one of the two day fields to sidestep the OR rule |
The ? operator exists precisely because of the OR problem: Quartz requires exactly one of the two day fields to be ?, which makes the ambiguity impossible to express. It is a good design correction — and one more reason to check which dialect you are writing before copying an expression from the internet.
Why it works in your shell and not in cron
This is the most common cron support question, and the cause is nearly always the same: cron does not run in your shell environment.
- PATH is minimal — typically
/usr/bin:/bin. Anything installed in/usr/local/bin, a language version manager, or a virtual environment will not be found. - No profile is read.
.bashrc,.bash_profileand.zshrcare all skipped, so nothing they export exists. - The working directory is the user's home, not wherever you happened to be.
- There is no terminal, so anything expecting interactive input hangs or fails.
%is special in crontab and must be escaped as\%. Unescaped, everything after it becomes standard input for the command — which is exactly whydate +%Y-%m-%dmysteriously produces nothing.
🚨 Two failure modes that hide themselves
Silent failure. Redirecting to /dev/null discards errors along with output. Cron mails output to the user by default, but on most servers that mail is never read. Log to a file and monitor it, or push to whatever alerting you already have.
Overlapping runs. A job scheduled every 5 minutes that takes 7 minutes will start a second copy before the first finishes, then a third — until the machine is saturated. flock -n makes a run exit immediately if the previous one is still going, and it is one line.
Timezones and daylight saving
Cron runs in the system timezone, which produces two specific problems twice a year:
- Spring forward. Clocks jump from 01:59 to 03:00. A job scheduled at 02:30 has no 02:30 to run at. Most modern cron implementations run it once anyway; older ones skip it entirely.
- Autumn back. 01:00 to 01:59 occurs twice. A job scheduled at 01:30 may run twice — which for a billing job or a data import is considerably worse than not running at all.
The fix is to run servers in UTC, which has no daylight saving, and convert to local time inside the application if users need it. Where the system timezone cannot be changed, avoid scheduling anything between 01:00 and 03:00 local time. If a job absolutely must run in that window, make it idempotent so a double execution is harmless.
Editing and verifying
That last line is the single most useful debugging trick with cron. Run it once, compare /tmp/cron-env.txt against the output of env in your own shell, and the difference will usually be the whole problem.
⚠️ A crontab needs a trailing newline
Older cron implementations silently ignore the final line of a crontab if the file does not end with a newline character. The job is present in the file, appears in crontab -l, and never runs. If your last entry is being ignored for no visible reason, add a blank line at the end.
When not to use cron
| Need | Better tool | Why |
|---|---|---|
| Sub-minute intervals | systemd timer, or a resident process | Cron's floor is one minute |
| Missed runs must catch up | anacron, systemd Persistent=true | Cron simply skips runs while the machine was off |
| Dependencies between jobs | A workflow scheduler | Cron has no concept of one job following another |
| Retries and backoff | A job queue | Cron does not know or care whether a job failed |
| Visibility across many machines | A managed scheduler | Cron is per-host with no central view |
| Ordering after network or mounts | systemd unit with After= | @reboot has no dependency model |
None of this makes cron obsolete. For "run this script at this time on this machine" it remains the simplest thing that works, it is installed everywhere, and it has no dependencies. The failure mode is using it for orchestration it was never designed to do.
Working with timestamps in your scripts?
Convert Unix timestamps to readable dates and back, in any timezone — instantly and in your browser.
Open the Timestamp Converter →The checklist
- Five fields: minute, hour, day-of-month, month, day-of-week.
- Restricting both day fields gives OR, not AND. The single biggest cron trap.
*/Ncounts from the start of the range, so it is uneven on any field that does not begin at zero.- Use absolute paths and set PATH explicitly — cron reads no profile.
- Escape
%as\%or your command is truncated. - Log output rather than discarding it, and use
flockto prevent overlap. - Run in UTC to avoid daylight-saving double-runs and skipped runs.
- Offset from the hour to spread load.
Frequently Asked Questions
What does * * * * * mean in cron?
Run every minute — the five fields are minute, hour, day of month, month and day of week, and an asterisk means 'every value' in that position. It is the most frequent schedule standard cron can express; anything more often than once a minute needs a different scheduler.
Why does my cron job run every day instead of once a month?
Because when both the day-of-month and day-of-week fields are restricted, cron combines them with OR, not AND. The expression '0 0 1 * 1' does not mean 'the 1st, if it is a Monday' — it means 'the 1st of the month, OR any Monday'. To require both, restrict only one field and check the other inside your script.
What is the difference between */15 and 0,15,30,45?
In the minute field, nothing — both run at :00, :15, :30 and :45. The step operator */15 means 'every 15th value starting from the first', and since minutes start at 0 the results match. They diverge on fields that do not start at zero: in the day-of-month field, */5 gives the 1st, 6th, 11th, 16th, 21st, 26th and 31st, not every fifth day evenly.
How do I run a cron job every 30 seconds?
Standard cron cannot — one minute is its finest resolution. The usual workaround is two entries, one running the job immediately and one with a 30-second sleep first. A better answer is a systemd timer with OnUnitActiveSec=30s, or a long-running process with its own internal loop.
Why does my script work manually but fail in cron?
Almost always the environment. Cron runs with a minimal PATH — typically just /usr/bin:/bin — and none of the variables your shell profile sets, because it does not read .bashrc or .profile. Use absolute paths for every command and file, and set any variables the script needs explicitly at the top of the crontab.