Cron Syntax Explained
A cron expression is five fields separated by spaces: minute, hour, day of month, month and day of week. Each field holds a number, an asterisk meaning “every”, or a combination built with commas, hyphens and slashes. So 30 2 * * 1 means 02:30 every Monday, and */15 * * * * means every 15 minutes. The same syntax is used by crontab on Linux and macOS, Kubernetes CronJobs, GitHub Actions and most hosting schedulers.
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 runHours use the 24-hour clock, so 6 p.m. is 18. Month and weekday names are three-letter English abbreviations and are not case sensitive. Names cannot be used in ranges on every implementation, so 1-5 is more portable than MON-FRI.
The four special characters
| Character | Meaning | Example | Result |
|---|---|---|---|
* | Every value | 0 * * * * | Every hour on the hour |
, | A list | 0 9,13,17 * * * | At 09:00, 13:00 and 17:00 |
- | A range | 0 9-17 * * 1-5 | Hourly from 09:00 to 17:00 on weekdays |
/ | A step | */10 * * * * | Every 10 minutes |
Steps count from the start of the range, not from when the job was installed. */10 fires at :00, :10, :20 and so on. A step that does not divide evenly restarts each hour or day: */25 in the minute field fires at :00, :25 and :50, then :00 again ten minutes later. To run something every 90 minutes you need two lines, because no single expression produces that rhythm.
Not sure what an expression does? Paste it into the Cron Expression Parser to get a plain-English reading and the next run times.
Shortcuts
| Shortcut | Equivalent | Meaning |
|---|---|---|
@hourly | 0 * * * * | Start of every hour |
@daily / @midnight | 0 0 * * * | Midnight every day |
@weekly | 0 0 * * 0 | Midnight on Sunday |
@monthly | 0 0 1 * * | Midnight on the 1st |
@yearly / @annually | 0 0 1 1 * | Midnight on 1 January |
@reboot | No equivalent | Once, when the cron daemon starts |
The day-of-month and day-of-week trap
When both day fields are restricted, standard cron runs the job if either matches. 0 0 13 * 5 does not mean “Friday the 13th”; it means every 13th of the month and every Friday. To get the intersection, schedule on one field and test the other inside the command, for example running every Friday and exiting unless date +%d is 13. If either field is an asterisk, the rule does not apply.
Why a job works in the terminal but not in cron
- A minimal environment. Cron does not load your shell profile.
PATHis often just/usr/bin:/bin, so commands installed elsewhere are not found. Use absolute paths, or setPATH=at the top of the crontab. - Unescaped percent signs. In a crontab line,
%is turned into a newline.date +%Y-%m-%dmust be writtendate +\%Y-\%m-\%d. - No execute permission. A script called directly needs its execute bit set, usually with
chmod 755. The Chmod Calculator shows what a mode allows. - Lost output. Errors are mailed to the local user, where nobody reads them. Redirect both streams to a log:
/path/to/job.sh >> /var/log/job.log 2>&1. - Relative paths. Cron starts jobs in the user's home directory, not the script's folder. Use
cd /path &&first or absolute file paths.
Time zones and daylight saving
A system crontab runs in the server's local time zone. Many hosted schedulers use UTC: GitHub Actions schedules always do, and Kubernetes CronJobs use the controller's zone unless you set the timeZone field. Daylight saving causes the remaining surprises. A job at 02:30 may be skipped on the night clocks spring forward and may run twice, or once, on the night they fall back, depending on the implementation. Run servers on UTC, or keep important jobs out of the 01:00 to 03:00 window. If you are checking log timestamps after the fact, the Unix Timestamp Converter turns epoch values into readable dates.
Cron dialects that look similar
| System | Fields | Differences |
|---|---|---|
| crontab (Vixie cron, cronie) | 5 | The reference syntax; system crontabs add a user column |
| Kubernetes CronJob | 5 | Standard syntax, optional timeZone field |
| GitHub Actions | 5 | Always UTC; shortest interval is 5 minutes; runs can be delayed at busy times |
| AWS EventBridge | 6 | Adds a year field; one day field must be ? |
| Quartz, Spring | 6 or 7 | Seconds come first; adds ?, L, W and # |
Copying an expression between these systems is the most common source of “it ran at the wrong time” bugs. A five-field expression pasted into Quartz shifts every field one place to the left.
Good habits
- Add a comment above each line saying what it does and who owns it.
- Avoid stampedes: if many machines run the same job, pick an odd minute such as 17 rather than 0.
- Prevent overlaps on long jobs with a lock, for example
flock -n /tmp/job.lock /path/to/job.sh. - Keep crontabs in version control, and test new expressions before deploying them.