Guides 8 min read

Cron Expressions Explained: Syntax, Examples and Common Mistakes

A cron expression is five fields that say when a job runs. The syntax, special characters and @daily shortcuts, ready-to-use examples and the timezone and PATH mistakes that break jobs.

A cron expression is five fields separated by spaces: minute, hour, day of month, month and day of week, followed by the command to run. 30 2 * * * means “at 02:30 every day”. Each field takes a number, a * for “every value”, a list with commas, a range with a hyphen or a step with a slash. Most cron problems do not come from the syntax itself but from what surrounds it: the server’s time zone, the rule for combining day of month and day of week, and the minimal environment cron runs commands in.

This guide explains each field and special character, gives a table of ready-to-use expressions and walks through the mistakes that make jobs run at the wrong time, or not at all.

The five fields

Every line in a user crontab follows the same layout:

┌───────────── 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)
│ │ │ │ │
* * * * * /path/to/command

A job runs when the current minute matches all the fields, with one exception for the two day fields covered below. Months and weekdays can be written as three-letter English names, which are case insensitive.

You edit your own crontab with crontab -e and list it with crontab -l. System-wide files, /etc/crontab and the files in /etc/cron.d/, have a sixth field between the schedule and the command: the user the job runs as. Copying a line from one format to the other without adding or removing that field is a classic way to break a job.

Special characters

CharacterMeaningExampleResult
*Every value* * * * *Every minute
,List of values0 8,12,18 * * *At 08:00, 12:00 and 18:00
-Range0 9 * * 1-509:00 Monday to Friday
/Step*/15 * * * *Minutes 0, 15, 30 and 45

How steps really work

A step divides the range of a field, starting from its first value. */15 in the minute field means 0, 15, 30 and 45. A step can also apply to a range: 0-30/10 means minutes 0, 10, 20 and 30.

Steps restart at the boundary of the field. */7 in the minute field runs at 0, 7, 14 and so on up to 56, then at 0 again, so the last gap of each hour is only four minutes. */7 is not “every seven minutes”. Classic cron cannot express “every 90 minutes” in one line either; you need two lines or a check inside the script.

Syntax that is not classic cron

You may see L (last), W (nearest weekday), # (nth weekday of the month) and ? in documentation. They come from Quartz and some cloud and CI schedulers, not from the cron on a Linux server, and some of those schedulers also add a seconds field at the start. Many crons also reject ranges of names, so write 1-5 rather than MON-FRI.

Shortcut strings like @daily

Most Linux cron implementations accept a named shortcut in place of the five fields:

  • @reboot: once, when the cron daemon starts after a boot.
  • @yearly or @annually: 0 0 1 1 *, midnight on 1 January.
  • @monthly: 0 0 1 * *, midnight on the first of the month.
  • @weekly: 0 0 * * 0, midnight between Saturday and Sunday.
  • @daily or @midnight: 0 0 * * *.
  • @hourly: 0 * * * *.

Shortcuts all fire on the hour or at midnight, so on a busy server every job competes for the same resources at 00:00. Writing the full expression with an odd minute, such as 17 0 * * *, spreads the load.

Ready-to-use cron expressions

ExpressionRuns
* * * * *Every minute
*/5 * * * *Every 5 minutes
*/15 * * * *Every 15 minutes
0 * * * *Every hour, on the hour
15 */2 * * *At minute 15 of every second hour: 00:15, 02:15, 04:15 and so on
0 9-17 * * 1-5Every hour from 09:00 to 17:00, Monday to Friday
*/10 8-18 * * 1-5Every 10 minutes during office hours on weekdays
30 2 * * *Every day at 02:30
0 8,12,18 * * *Three times a day: 08:00, 12:00 and 18:00
0 6 * * 1Every Monday at 06:00
0 22 * * 5Every Friday at 22:00
0 0 * * 0Every Sunday at midnight
0 3 1 * *The first day of every month at 03:00
0 4 1,15 * *The 1st and 15th of every month at 04:00
0 5 1 */3 *Quarterly: 1 January, April, July and October at 05:00
0 0 1 1 *Once a year, at midnight on 1 January
@rebootOnce after every system start

A common mistake in this list is * */2 * * *. It does not run every two hours. It runs every minute during every second hour, sixty times in a row. When you restrict the hour, always set the minute too.

Day of month and day of week: the OR rule

All fields are combined with AND, except the two day fields. When both day of month and day of week are restricted, that is neither is *, the job runs when either matches. So this line:

0 9 1 * 1 /usr/local/bin/report.sh

runs at 09:00 on the first of every month and at 09:00 every Monday, not only on Mondays that fall on the first. For the same reason, 0 9 1-7 * 1 does not mean “the first Monday of the month”; it runs on days 1 to 7 and on every Monday. To get the first Monday, schedule days 1 to 7 and check the weekday in the command. Note the escaped percent sign, explained further down:

0 9 1-7 * * [ "$(date +\%u)" = "1" ] && /usr/local/bin/report.sh

Classic cron has no “last day of the month” either. With GNU date, schedule days 28 to 31 and run only when tomorrow is the first:

0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /usr/local/bin/month-end.sh

Time zones and daylight saving time

Cron uses the time zone of the system it runs on. A job written for Stockholm time on a server set to UTC starts an hour or two off, depending on the season. Check the server’s zone with timedatectl or date before you choose a schedule.

Some implementations, such as cronie, accept a CRON_TZ variable at the top of the crontab to run the schedules in another zone. Support differs between cron versions, so test it rather than assuming it works.

Daylight saving time causes the strangest bugs. In spring, one local hour does not exist; in autumn, one hour happens twice. A job scheduled between roughly 01:00 and 03:00 local time can be skipped, delayed or run twice, and how each cron implementation handles the change varies. The safest options:

  • Run servers on UTC, which has no daylight saving, and convert in your head or in a comment.
  • If the server must run on local time, keep important jobs outside the hours when the clocks change.
  • Write the time zone in a comment above each job, so the next person does not have to guess.
  • Make jobs safe to run twice, for example by checking whether today’s export already exists.

Environment and PATH gotchas

A command that works in your terminal and fails under cron almost always fails because of the environment. Cron does not load your shell profile, so a lot of what you take for granted is missing:

  • PATH is minimal, often just /usr/bin:/bin. Commands in /usr/local/bin, or tools installed with nvm, pyenv, rbenv or Composer, are not found. Use absolute paths or set PATH at the top of the crontab.
  • The shell is /bin/sh, not bash. Bash-only syntax such as [[ ]] or source fails. Set SHELL=/bin/bash or put the logic in a script with a bash shebang.
  • Percent signs are special. In a crontab line, % is turned into a newline. date +%F must be written date +\%F, or moved into a script.
  • The working directory is the user’s home, not your project. Relative paths point to the wrong place. cd into the project first.
  • Environment variables are gone. API keys and database URLs exported in .bashrc do not exist. Load them from a file in the script.
  • Output disappears. Cron mails output to MAILTO if mail is set up, and on most servers it is not. Redirect output to a log file.
  • Runs can overlap. If a job every five minutes sometimes takes seven, two copies run at once. flock -n skips a run while the previous one holds the lock.

Some cron versions also ignore a last line that does not end with a newline. A crontab that handles all of this looks like:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

# Nightly database backup. Server time is UTC.
30 2 * * * cd /var/www/fjordstudio && flock -n /tmp/backup.lock ./bin/backup.sh >> /var/log/backup.log 2>&1

How to test a cron expression

  1. Read it back in words. Paste the expression into a cron expression parser and check that the description and the next few run times match what you meant.
  2. Run the command in cron’s environment. A stripped environment reproduces most “works in my terminal” failures before you wait for the schedule.
  3. Schedule it every minute once. Use * * * * * temporarily, watch it run, then set the real schedule.
  4. Check that cron started it. On Debian and Ubuntu, grep CRON /var/log/syslog or journalctl -u cron; on Red Hat based systems, /var/log/cron or journalctl -u crond.
  5. Confirm the crontab is installed with crontab -l, as the user that should run it.

To simulate cron’s environment from a shell:

env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME="$HOME" /bin/sh -c '/usr/local/bin/backup.sh'

Pair every job with a heartbeat

A correct expression only tells cron when to start a job. It says nothing about whether the job ran or finished. A reboot, an overwritten crontab or a missing variable all fail silently.

A heartbeat closes that gap. The job requests a unique URL when it finishes successfully, and the monitoring service alerts you when the request does not arrive on schedule:

30 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 https://heartbeat.example.com/your-unique-id > /dev/null

Set the heartbeat’s expected interval to the same schedule as the cron expression, with margin for slow runs: 24 hours plus a little for a nightly job, one hour plus a little for an hourly one. Uptimecheck cron job monitoring works this way, with alerts when a ping is missing and a recovery notice when the job checks in again. The guide on how to monitor cron jobs with heartbeats covers the setup in detail, and preventing missed cron tasks covers the habits around it.

When a heartbeat goes missing, the cause is often on the server: a full disk, exhausted memory or an unplanned reboot. Server monitoring next to your heartbeats shows that cause at the same moment.

Common mistakes checklist

  1. Restricting the hour but leaving the minute as *.
  2. Setting both day fields and expecting AND instead of OR.
  3. Assuming the server is in your time zone.
  4. Scheduling important jobs in the daylight saving hour.
  5. Using relative paths, bash syntax or unescaped %.
  6. Running a job with no heartbeat, so nobody notices when it stops.

Summary

Five fields, four special characters and a handful of shortcuts cover almost every schedule you will need. The expression is rarely the problem. Time zones, the OR rule for day fields and cron’s bare environment are. Write the time zone next to each job, use absolute paths, log the output and give every job that matters a heartbeat.

Uptimecheck heartbeats are included on every plan, next to uptime, server, SSL, domain and DNS monitoring. Start a 14-day free trial, no credit card required.

Questions

What does */5 * * * * mean in cron?

Every five minutes: at minutes 0, 5, 10 and so on through 55 of every hour, every day.

How do I run a cron job every two hours?

Use 0 */2 * * *, which runs at minute 0 of every second hour. Writing * */2 * * * runs every minute during every second hour instead.

Which time zone does cron use?

The time zone of the system it runs on. Many cloud servers and containers use UTC, so check with timedatectl or date before choosing a schedule. Some implementations support a CRON_TZ variable.

Why does my cron job work in the terminal but not in cron?

Cron runs commands with a minimal PATH, /bin/sh as the shell, the home directory as working directory and without your shell profile. Use absolute paths, escape % signs and log the output to see the error.

Keep reading

Guides 5 min read

How to Monitor Cron Jobs with Heartbeats

Cron jobs fail silently. A heartbeat monitor alerts you when a scheduled job does not check in on time. How heartbeats work, how to add one to a crontab and which settings to choose.

Read

Downtime is obsolete.

Set up your first monitor in under a minute and know before your customers do.

14-day free trial · No credit card required

Start free trial