Blog

Crontab syntax explained: every field and special character, with examples

Crontab syntax in one page: the five fields, the special characters * , - /, the @daily shortcuts, the day-of-month OR day-of-week trap, the percent sign, and a table of ready-to-copy schedules.

Published on October 20, 2026

Crontab syntax looks simple: five fields and a command. Most broken schedules still come from those five fields, because a few rules are not what you would guess. This page covers every field, every special character, the shortcuts like @daily, and the two traps that catch experienced admins: the day-of-month and day-of-week combination, and the percent sign.

This is the third post in our Cron in production series. The first one covered why a job does not run, the second one where to find the evidence. If you only want to check one expression, paste it into the cron expression parser: it splits it into fields and shows the next five runs in your timezone.

The five fields

A line in a user crontab (crontab -e) has five time fields followed by the command:

┌───────────── 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 both Sunday)
│ │ │ │ │
30 2 * * 1-5  /opt/scripts/report.sh

That line runs report.sh at 02:30 on weekdays. The time is the time of the machine (or the CRON_TZ variable, where the cron implementation supports it), not UTC and not your laptop’s time zone.

A job runs when the current minute matches all fields, with one exception covered below. Cron checks once a minute, so the smallest interval is one minute. There is no seconds field in standard cron.

Special characters

CharacterMeaningExampleRuns
*Every value* * * * *Every minute
,List0 8,12,18 * * *At 08:00, 12:00 and 18:00
-Range, inclusive0 9-17 * * *Every hour from 09:00 to 17:00
/Step*/15 * * * *At minutes 0, 15, 30 and 45
a-b/nStep within a range0 8-18/2 * * *08:00, 10:00, … 18:00
namesMonths and days0 0 * jan,jul monMidnight on Mondays in January and July

You can combine them in one field: 0,30 9-17 * * 1-5 runs every half hour during office hours on weekdays.

Steps start at the lowest value, not at “now”

*/n means “every value divisible by n, counting from the start of the field”. It does not mean “every n units from when I saved the crontab”. Two consequences:

To start a step somewhere else, use a range: 5-59/10 * * * * runs at 5, 15, 25, 35, 45 and 55.

Names are not case sensitive, but cannot be ranges everywhere

mon, MON and Mon all work. Ranges of names such as mon-fri work in Vixie cron and cronie (Debian, Ubuntu, RHEL), but some minimal implementations, including older BusyBox builds used in Alpine images, only accept numbers. If the crontab has to run in containers too, 1-5 is the safer form.

The trap: day of month OR day of week

When both the day-of-month and the day-of-week field are restricted (neither is *), cron runs the job when either matches, not when both match.

0 9 1-7 * 1

Most people read this as “09:00 on the first Monday of the month”. Cron reads it as “09:00 on days 1 to 7 of the month, and 09:00 on every Monday”. That is up to eleven runs a month instead of one.

Standard cron cannot express “first Monday” on its own. The usual workaround is to schedule on the days and check the weekday in the command:

0 9 1-7 * * [ "$(date +\%u)" = 1 ] && /opt/scripts/monthly.sh

Note the \%, which brings us to the second trap.

The trap: the percent sign

In a crontab line, an unescaped % ends the command. Everything after it is passed to the command as standard input, with further % turned into newlines. So this line:

0 0 * * * tar czf /backup/site-$(date +%F).tgz /var/www

runs tar czf /backup/site-$(date + and feeds the rest to its input. The job fails, and because the output goes to mail, often nobody sees why. Escape every percent sign as \%, or move the command into a script and call the script from the crontab.

Shortcuts: @daily, @reboot and friends

Most cron implementations accept these instead of the five fields:

ShortcutSame asNote
@yearly / @annually0 0 1 1 *Midnight, January 1st
@monthly0 0 1 * *Midnight, first day of the month
@weekly0 0 * * 0Midnight between Saturday and Sunday
@daily / @midnight0 0 * * *
@hourly0 * * * *
@rebootnoneOnce, when the cron daemon starts

@reboot runs when cron starts, which is usually boot, but also after a restart of the cron service. It is not a replacement for a proper service unit, and in containers it runs every time the container starts.

Be careful with @daily on many servers at once: they all fire at exactly 00:00, which is how backups end up competing for the same disk or the same API. Picking a random minute and hour per server (17 3 * * *) spreads the load.

User crontab vs /etc/crontab and /etc/cron.d

The system crontab and the files in /etc/cron.d/ have a sixth field before the command: the user that runs it.

# crontab -e (user crontab): no user field
30 2 * * * /opt/scripts/backup.sh

# /etc/crontab and /etc/cron.d/*: user field
30 2 * * * root /opt/scripts/backup.sh

Copying a line from one to the other without adding or removing the user field is a classic way to get a job that never runs: in /etc/cron.d/ the command /opt/scripts/backup.sh would be treated as a user name. Files in /etc/cron.d/ also have naming rules (no dots on Debian) and must end with a newline, covered in the first post of the series.

Ready-to-copy schedules

ScheduleExpression
Every 5 minutes*/5 * * * *
Every hour at minute 00 * * * *
Every 2 hours0 */2 * * *
Every day at 02:3030 2 * * *
Twice a day, 06:00 and 18:000 6,18 * * *
Weekdays at 09:000 9 * * 1-5
Every 15 minutes during office hours on weekdays*/15 9-17 * * 1-5
Saturday and Sunday at 10:000 10 * * 6,0
First day of the month at 00:000 0 1 * *
Every quarter, first day at 06:000 6 1 1,4,7,10 *
Last day of the month0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && cmd

Paste any of them into the cron parser to see the next runs before you save the crontab. The last one uses GNU date; on BusyBox, check the day of month in the script instead.

Common mistakes

  1. * 2 * * * instead of 0 2 * * *. The first runs every minute from 02:00 to 02:59, sixty times.
  2. Restricting both day fields and expecting AND (see above).
  3. An unescaped % in the command.
  4. */n with n that does not divide 60 or 24, expecting an even interval.
  5. A user field in crontab -e or a missing one in /etc/cron.d/.
  6. Six fields copied from another scheduler. Quartz, Spring and some cloud schedulers add seconds as the first field, and some add a year. Standard cron rejects the line, or worse, shifts every field by one.
  7. Assuming UTC. Cron uses the server’s time zone, including daylight saving changes: a job at 02:30 can run twice or not at all on the night the clocks move.

Correct syntax is not the same as a job that runs

A valid expression only says when cron should start the command. It does not tell you whether it actually started, finished, or failed at 02:30 while everyone was asleep. That is what a heartbeat monitor is for: the job pings when it starts and ends, and you get an alert when a ping is late or the exit code is not zero.

With HelloCron that is two curl calls around the existing command, or one wrapper that also reports the exit code:

30 2 * * * /usr/local/bin/hellocron run nightly-backup /opt/scripts/backup.sh

Monitor a cron job in 60 seconds shows the setup, and the cron jobs guide covers the details. The free plan includes 15 monitors.

Quick reference

  1. Five fields: minute, hour, day of month, month, day of week. Then the command.
  2. * every, , list, - range, / step from the lowest value.
  3. Both day fields restricted means OR, not AND.
  4. Escape % as \%, or call a script.
  5. /etc/crontab and /etc/cron.d/ need a user field, crontab -e does not.
  6. Check the expression in the cron parser before saving.

Next in the series: cron environment variables, why your script works in the shell but not in cron, and how to give cron the environment it needs.

← Back to blog

Start monitoring in 60 seconds

Sign up, get an API key, paste the curl into your cron. That's it.