Cron Expression Syntax: Fields, Examples, and Common Mistakes
Updated
Contents
A cron expression is five space-separated fields — minute, hour, day of month, month, and day of week — that tell cron when to run a command. 30 2 * * 1-5 means 02:30 every Monday through Friday. * matches every value, , separates a list, - makes a range, and / steps through one. To run a job every 2 months, use 0 0 1 */2 *: midnight on the 1st of January, March, May, July, September, and November.
The rules below follow crontab(5) from cronie, the cron daemon on Fedora, RHEL, and Arch. Debian and Ubuntu ship a Vixie-derived cron with the same five fields and operators. Kubernetes, GitHub Actions, Quartz, Spring, and AWS reuse the syntax with differences, covered near the end. For how the cron daemon finds and runs these lines, see what is a cron job.
The Five Fields#
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12 or JAN-DEC)
│ │ │ │ ┌───────────── day of week (0-7 or SUN-SAT; 0 and 7 are Sunday)
│ │ │ │ │
* * * * * command to run| Field | Allowed values | Names |
|---|---|---|
| Minute | 0-59 | |
| Hour | 0-23 | |
| Day of month | 1-31 | |
| Month | 1-12 | JAN to DEC |
| Day of week | 0-7 (0 and 7 are Sunday) | SUN to SAT |
Names are the first three letters of the month or day, in any case. Hours use a 24-hour clock, so 2 PM is 14.
Operators#
| Operator | Meaning | Example | Matches |
|---|---|---|---|
* |
Every value | * in hour |
Every hour |
, |
List | 1,15 in day of month |
The 1st and the 15th |
- |
Inclusive range | 1-5 in day of week |
Monday through Friday |
/ |
Step through every value | */15 in minute |
:00, :15, :30, :45 |
/ |
Step through a range | 9-17/2 in hour |
09:00, 11:00, 13:00, 15:00, 17:00 |
A step counts from the start of its range, not from when you saved the crontab. */15 in the minute field always means :00, :15, :30, and :45, whatever time it is now.
Cron Job Expression vs. Crontab Line#
A crontab line is a cron expression followed by the command to run:
# minute hour day-of-month month day-of-week command
0 2 * * * /opt/app/backup.shSystem crontabs (/etc/crontab and files in /etc/cron.d/) add a user between the expression and the command: 0 2 * * * appuser /opt/app/backup.sh. Mixing up the two formats is a common reason a cron job is not running. The crontab guide covers editing, listing, and backing up both kinds.
In the command part, an unescaped % becomes a newline, and everything after the first one is sent to the command as standard input. Write \% instead, as in date +\%F, or move the command into a script.
Common Cron Expressions#
| Schedule | Cron expression |
|---|---|
| Every minute | * * * * * |
| Every 5 minutes | */5 * * * * |
| Every 15 minutes | */15 * * * * |
| Every hour, on the hour | 0 * * * * |
| Every 6 hours | 0 */6 * * * |
| Every day at midnight | 0 0 * * * |
| Every day at 02:30 | 30 2 * * * |
| Weekdays at 09:00 | 0 9 * * 1-5 |
| Every Sunday at 03:00 | 0 3 * * 0 |
| 1st of every month at midnight | 0 0 1 * * |
| 1st and 15th of every month at 06:00 | 0 6 1,15 * * |
| Every 2 months, on the 1st at midnight | 0 0 1 */2 * |
| Every quarter (Jan, Apr, Jul, Oct) | 0 0 1 */3 * |
| Once a year, January 1 at midnight | 0 0 1 1 * |
How to Run a Job Every 2 Months#
Put a step in the month field:
0 0 1 */2 * /opt/app/bimonthly-report.shThat runs at 00:00 on the 1st of January, March, May, July, September, and November. For the even months, start the range at February: 0 0 1 2-12/2 * runs in February, April, June, August, October, and December.
A step restarts at the end of its range, so it only gives an even interval when it divides the range. Every 2, 3, 4, or 6 months works because each divides 12. */5 in the month field means January, June, and November, then January again two months later. The minute field behaves the same way: */7 runs at :56 and then at :00, four minutes apart.
Cron has no memory of the last run, so it can't express "every other week" or "every 10 days" either. Schedule the job more often and let a small guard decide whether this run does the work:
#!/usr/bin/env bash
# Cron runs this daily; the work happens at most once every 14 days.
set -euo pipefail
stamp=/var/lib/myapp/last-run
if [ -f "$stamp" ] && [ $(( $(date +%s) - $(stat -c %Y "$stamp") )) -lt $(( 14 * 86400 - 3600 )) ]; then
exit 0
fi
/opt/app/fortnightly-task.sh
touch "$stamp"The one-hour allowance stops a run that took a few minutes from pushing the next one a day later. Because the stamp is written only after the task succeeds, a failed run is retried the next day.
A job that runs six times a year is the easiest kind to lose. If it stops, nothing complains until someone needs what it was supposed to produce, possibly two months later. Give infrequent jobs a deadline; the last section shows how.
Day of Month and Day of Week Together#
When both day fields are restricted, cronie runs the command when either one matches. 0 9 1 * MON runs at 09:00 on the 1st of every month and every Monday, not only on Mondays that fall on the 1st.
For "the first Monday of the month", restrict the day of month to 1-7 and check the weekday in the command:
0 9 1-7 * * [ "$(date +\%u)" = 1 ] && /opt/app/monthly-review.shCron last day of month covers the same date test for the last day of the month and the last weekday.
Not every scheduler uses the either-field rule. Quartz and AWS EventBridge make you put ? in one of the two fields, and CronRadar requires a date to match both. See CronRadar's schedule syntax before monitoring a schedule that restricts both.
Shortcuts: @daily, @reboot, and Others#
cronie accepts these in place of the five fields:
| Shortcut | Equivalent |
|---|---|
@yearly or @annually |
0 0 1 1 * |
@monthly |
0 0 1 * * |
@weekly |
0 0 * * 0 |
@daily |
0 0 * * * |
@hourly |
0 * * * * |
@reboot |
Once, at startup |
Kubernetes CronJobs accept the same shortcuts except @reboot. Many libraries and services accept none of them, so write the five fields when you aren't sure. @reboot in cron covers when an @reboot line runs and why it can fail at boot.
Time Zones and Daylight Saving Time#
Cron uses the time zone of the cron daemon, normally the system zone in /etc/localtime. cronie also lets a crontab set CRON_TZ for the entries below it; Debian's cron and BusyBox crond have no equivalent. On a server set to UTC, 0 9 * * * runs at 09:00 UTC, which is 04:00 or 05:00 in New York depending on daylight saving time. Cron timezone shows how to check the daemon's zone and run one job in another.
Daylight saving affects only jobs in the hour the clocks skip or repeat, and the implementations disagree. For entries in the daemon's own zone, cronie and Debian's cron run a fixed-time job from the skipped hour right after the jump and run one in the repeated hour once, as their cron(8) pages say. cronie's crontab(5) says instead that skipped times never match and repeated times run twice. In the 1.7.2 source, the catch-up logic covers only the daemon's zone (cron.c), so crontab(5) describes CRON_TZ entries (cron.c), and BusyBox crond treats every entry that way. Interval schedules such as */15 * * * * run through both passes of the repeated hour. Cron daylight saving time has the full table. Keep important jobs out of the 01:00-03:00 local window, or run the server on UTC.
Hosted schedulers set their own rules:
- GitHub Actions runs
scheduleworkflows in UTC unless you give an IANAtimezone. The shortest interval is every 5 minutes, and runs can be delayed under load. See GitHub's schedule event docs. - Kubernetes CronJobs use the kube-controller-manager's local time zone unless
.spec.timeZoneis set.CRON_TZorTZinside the schedule is rejected. See Kubernetes CronJob time zones. - CronRadar evaluates monitor schedules in UTC, so write the expression you send it in UTC.
Five, Six, or Seven Fields#
| Scheduler | Fields | Differences from Linux cron |
|---|---|---|
| Linux cron (cronie) | 5 | Baseline |
| Kubernetes CronJob | 5 | Day of week 0-6; ? means *; .spec.timeZone |
| GitHub Actions | 5 | UTC by default; every 5 minutes at most |
Spring CronExpression |
6, seconds first | Adds L, W, #, ? |
| Quartz | 6 or 7, seconds first, optional year | Day of week 1-7 with Sunday = 1; ? required in one day field; L, W, # |
| AWS EventBridge Scheduler | 6, year last, inside cron(...) |
? required in one day field; L, W, # |
| CronRadar | 5 | No shortcuts, L, W, #, or ?; both day fields must match; UTC |
Cron's 0 2 * * * is 0 0 2 * * ? in Quartz. Copying an expression between systems without converting it gets you a schedule that is rejected or, worse, accepted with a different meaning: 1 in the day-of-week field is Monday in cron and Sunday in Quartz. Spring @Scheduled cron jobs covers Spring's six-field form.
Check an Expression Before You Deploy It#
Paste it into crontab.space, a free cron expression generator and validator, to read it back in plain English and preview the next run times. After installing it, run crontab -l as the user that owns the job to confirm cron received the line you meant. If crontab refuses the line with bad minute, bad command, or another message, crontab error messages explains what each one means.
Know When the Schedule Doesn't Run#
A valid expression only proves cron will try. It doesn't show that the job started, finished, or did its work: the server can be off, a deploy can overwrite the crontab, or the script can fail on its first line. Cron reports none of this.
How to know if a cron job ran compares the ways to check. The one that catches a job that never started is a dead man's switch: the job checks in after each run, and a monitor that knows the schedule alerts when a check-in is late. With CronRadar, a check-in can carry the schedule, and the first one creates the monitor. Put the check-in in a small wrapper script so the encoded % signs stay out of the crontab:
#!/usr/bin/env bash
# /opt/app/bimonthly-report-monitored.sh: run the report, then report its success.
/opt/app/bimonthly-report.sh || exit $?
curl --fail --silent --show-error --max-time 5 \
--user "$CRONRADAR_API_KEY:" \
"https://cron.life/api/v1/ping/bimonthly-report?schedule=0%200%201%20*/2%20*" || trueSet CRONRADAR_API_KEY in the job's environment and schedule the wrapper instead of the report:
0 0 1 */2 * /opt/app/bimonthly-report-monitored.shA failed report exits with its own code and sends nothing, and an unreachable CronRadar never fails the job. Because every check-in carries the schedule, the monitor recreates itself if it is ever deleted.
If January's report doesn't arrive, you hear about it in January, not in March.
Frequently Asked Questions#
What is a cron expression?#
Five fields — minute, hour, day of month, month, and day of week — that describe when a scheduled job runs. In a crontab, the command to run follows the expression on the same line.
Does cron support seconds?#
No. Standard cron's smallest unit is one minute. Spring and Quartz add a seconds field at the start, which is why their expressions have six or seven fields. Work that must run more often than once a minute usually belongs in a long-running worker; cron every 30 seconds compares that with the workarounds.
What does */5 mean in a cron expression?#
Every fifth value of the field, counted from the start of its range. In the minute field, */5 runs at :00, :05, :10, and so on through :55.
How do I run a cron job every 2 months?#
Use 0 0 1 */2 * for midnight on the 1st of January, March, May, July, September, and November, or 0 0 1 2-12/2 * for the even months.
Can cron run a job every other week?#
Not with the expression alone, because cron doesn't remember the last run. Run the job weekly or daily and have it check how long it has been since its last successful run, as in the guard script above.
Put this guide to work
Know when the next run is missing
Connect one scheduled job, confirm a real completion, then verify the alert destination. Keep your existing scheduler in charge of running the work.