Cron is the oldest scheduler still in daily use, and its five-field syntax has outlived almost every tool built to replace it. You will meet it in Linux crontabs, Kubernetes CronJobs, GitHub Actions, CI systems and countless cloud consoles. The syntax is tiny, which is exactly why it is easy to get subtly wrong. This guide explains how each part works, shows real schedules you can adapt, and covers the traps that cause jobs to run at the wrong time or not at all.
If you want to check an expression as you read, open the cron expression generator and explainer. It translates any schedule into plain English and lists the next run times.
The five fields
A standard cron expression is five values separated by spaces. Reading left to right they are:
| Position | Field | Allowed values |
|---|---|---|
| 1 | Minute | 0-59 |
| 2 | Hour | 0-23 |
| 3 | Day of month | 1-31 |
| 4 | Month | 1-12 (or JAN-DEC) |
| 5 | Day of week | 0-7 (or SUN-SAT), 0 and 7 are Sunday |
A job runs when the current time matches every field. The expression 30 14 * * * therefore means “at minute 30 of hour 14, on any day, in any month, on any weekday”, which is 14:30 every day. A field set to * places no restriction at all.
Note the order. People often write hour before minute by instinct, but cron wants minute first. 14 30 * * * is not 14:30; it is minute 14 of hour 30, which is invalid because hours stop at 23.
The four special characters
Standard cron has only four operators, and nearly every schedule is built from them.
The asterisk * means “every possible value”. * * * * * runs every minute.
The comma , separates a list. 0 9,13,17 * * * runs at 09:00, 13:00 and 17:00 every day.
The hyphen - defines an inclusive range. 0 9-17 * * * runs at the top of every hour from 09:00 to 17:00, which is nine runs per day.
The slash / defines a step. */15 * * * * means “every 15th minute”, so the job fires at minutes 0, 15, 30 and 45. You can combine a step with a range: 10-30/5 * * * * fires at minutes 10, 15, 20, 25 and 30.
You can mix operators within one field. 0,30 8-18/2 * * 1-5 runs at minutes 0 and 30 of the hours 8, 10, 12, 14, 16 and 18, Monday to Friday.
A step is not an interval
The most common misunderstanding is treating */N as “every N minutes since the last run”. It is not. A step walks through the values of the field starting from its minimum. */45 * * * * fires at minute 0 and minute 45 of every hour, so the gaps are 45 minutes, then 15 minutes, then 45 again. Steps only produce even spacing when N divides evenly into the range: 5, 10, 15, 20 and 30 for minutes, and 2, 3, 4, 6, 8 and 12 for hours. If you really need an uneven interval such as “every 90 minutes”, cron cannot express it. Use a scheduler with interval semantics, or run the job every 30 minutes and have the script skip unwanted runs.
Many implementations, including Kubernetes, also accept 5/15 to mean “start at 5, then every 15”. Classic Vixie cron does not, so write 5-59/15 if the file must be portable.
Names and macros
Month and weekday fields accept three-letter English names, which are easier to read: 0 8 * * MON-FRI is the same as 0 8 * * 1-5. Ranges cannot wrap around, so SAT-SUN is invalid; write SAT,SUN instead.
Most cron daemons also support shortcuts that replace all five fields:
@hourlyis0 * * * *@dailyor@midnightis0 0 * * *@weeklyis0 0 * * 0@monthlyis0 0 1 * *@yearlyor@annuallyis0 0 1 1 *@rebootruns once at startup and has no equivalent five-field form
Real schedules, decoded
| Expression | Meaning |
|---|---|
0 2 * * * |
Every day at 02:00, a popular slot for backups |
*/5 * * * * |
Every five minutes |
0 9 * * 1-5 |
09:00 on weekdays |
0 0 1 * * |
Midnight on the first day of each month |
0 0 * * 0 |
Midnight every Sunday |
30 3 1,15 * * |
03:30 on the 1st and 15th of the month |
0 */6 * * * |
At minute 0 of every sixth hour: 00:00, 06:00, 12:00, 18:00 |
15 10 * 6-8 * |
10:15 every day from June to August |
Notice the pattern for “once a day” jobs: set minute and hour to specific numbers and leave the rest as *. A frequent bug is writing * 2 * * *, which looks like “at 2 AM” but actually runs every minute between 02:00 and 02:59, sixty times instead of once. Always pin the minute.
The day-of-month and day-of-week trap
When you restrict both the day-of-month and the day-of-week fields, classic cron does not require both to match. It runs the job when either matches. So 0 0 13 * 5 fires on every 13th of the month and on every Friday, not just on Friday the 13th.
If you need a true AND, schedule the job on the more specific field and test the other condition in the command. For “the first Monday of the month”, 0 9 1-7 * 1 is wrong for exactly the reason above: it fires on days 1 to 7 and on every Monday. Instead, schedule every Monday and let the command check the date:
0 9 * * 1 [ "$(date +\%d)" -le 7 ] && /opt/job.sh
The \% is an escaped percent sign, explained in the pitfalls section below.
Time zones and daylight saving
Cron has no concept of a time zone inside the expression. It uses whatever zone the daemon runs in, which on servers and containers is very often UTC. A job set for 0 9 * * * on a UTC server runs at 09:00 UTC, which is 14:30 in India or 05:00 in New York in winter.
Daylight saving makes local-time schedules risky. When clocks jump forward, a job set for 02:30 may be skipped or run late; when clocks fall back, an hour repeats. Behaviour differs between cron implementations. The reliable fixes are to run the host in UTC and convert, or to avoid scheduling jobs between 01:00 and 03:00 local time.
Kubernetes CronJobs follow the kube-controller-manager’s time zone unless you set the timeZone field (stable since Kubernetes 1.27), for example timeZone: "Europe/Berlin". GitHub Actions scheduled workflows always use UTC and cannot be changed.
The same syntax in different places
crontab. Edit your personal table with crontab -e. Each line is the five fields followed by the command. Files in /etc/cron.d/ add a sixth column for the user to run as.
Kubernetes. A CronJob has spec.schedule: "*/10 * * * *". Set concurrencyPolicy to Forbid or Replace if overlapping runs would cause harm.
GitHub Actions. Use on: schedule: - cron: '0 6 * * 1'. Runs can start several minutes late when the platform is busy, the shortest supported interval is five minutes, and scheduled workflows only run from the default branch.
Cloud schedulers. AWS EventBridge uses a six-field format with a year and a ? wildcard, such as cron(0 12 * * ? *). Quartz and Spring use a leading seconds field. If an expression has six or seven parts, it is not standard cron, and you must convert it before pasting it into a crontab.
Pitfalls that bite in production
Percent signs. In a crontab line, an unescaped % is treated as a newline. A command like date +%Y-%m-%d breaks. Write date +\%Y-\%m-\%d, or move the command into a script.
A minimal environment. Cron does not load your shell profile. PATH is typically just /usr/bin:/bin, so a command that works in your terminal can fail with “command not found”. Use absolute paths or set PATH= at the top of the crontab.
Silent failures. By default cron mails output to the local user, which nobody reads. Redirect output explicitly: */5 * * * * /opt/job.sh >> /var/log/job.log 2>&1.
Overlapping runs. If a job sometimes takes longer than its interval, copies pile up. Wrap it with flock: */5 * * * * flock -n /tmp/job.lock /opt/job.sh.
Thundering herds. Dozens of jobs on 0 * * * * all start together. Stagger minutes (7, 23, 41) for non-urgent work.
A debugging checklist
- Paste the expression into the cron explainer and confirm the plain-English sentence is what you intended.
- Check the next five run times in the time zone your server actually uses.
- Run the command by hand using the same user and a stripped-down environment:
env -i /bin/sh -c '/opt/job.sh'. - Look at the system log:
grep CRON /var/log/syslogon Debian and Ubuntu, orjournalctl -u cronandjournalctl -u crondelsewhere. - Confirm the daemon is running and the crontab file ends with a newline, because some versions ignore a last line without one.
Where to go next
Once the syntax feels natural you can read any schedule at a glance: left to right, minute to weekday, each field narrowing the match. Bookmark the cron expression generator for the moments when it does not, and read Subnetting Explained for DevOps Engineers next if you are also planning infrastructure networks.