Skip to content

Cron Expression Generator

A cron expression is five whitespace-separated fields — minute, hour, day of month, month, day of week — that tell the cron daemon when to run a command. This generator builds one from those fields, translates any expression you paste into plain English, and shows the next five times it will actually fire.

0–59

0–23

1–31

1–12 or JAN–DEC

0–7 (0 and 7 = Sun) or SUN–SAT

Presets

Expression

Valid
*/15 * * * *

Every 15 minutes, every day.

Next 5 runs

Computed in your browser’s timezone (UTC). Your server almost certainly runs cron in UTC or its own TZ — check with timedatectl before trusting these times for production.

#Local date & timeISO
111/12/2026, 6:15:00 PM2026-11-12T18:15:00.000Z
211/12/2026, 6:30:00 PM2026-11-12T18:30:00.000Z
311/12/2026, 6:45:00 PM2026-11-12T18:45:00.000Z
411/12/2026, 7:00:00 PM2026-11-12T19:00:00.000Z
511/12/2026, 7:15:00 PM2026-11-12T19:15:00.000Z

The five fields

PositionFieldRangeNotes
1Minute0–59
2Hour0–2324-hour clock; midnight is 0
3Day of month1–31ORed with day of week when both are restricted
4Month1–12 or JAN–DECNames are case-insensitive, three letters
5Day of week0–7 or SUN–SATBoth 0 and 7 mean Sunday

Each field accepts four operators. * means every value. A comma makes a list: 1,15,30. A hyphen makes an inclusive range: 9-17. A slash adds a step: */15 in the minute field means 0, 15, 30, 45, and 1-23/2 means every odd hour. Steps count from the start of the range, not from "now" — */20 fires at :00, :20 and :40, never at :07, :27 and :47.

A step never spans a wrap-around. */40 in the minute field means minutes 0 and 40 only — after :40 the next value would be :80, which does not exist, so the hour restarts and you get a 20-minute gap. If you need an even split, pick a divisor of 60.

The day-of-month / day-of-week trap

This is the single most misunderstood rule in cron, and it is genuinely counter-intuitive: when both the day-of-month and day-of-week fields are restricted, cron ORs them rather than ANDing them. 0 0 1 * MON does not mean "the first of the month, if it is a Monday". It means "the first of every month, and additionally every Monday" — roughly five times as often as intended.

Read the full explanation of cron expression generator results ↓

When only one of the two is restricted, it simply applies. Vixie cron (the implementation behind cronie, Debian's cron, and most others) decides "restricted" by whether the field starts with *, so */2 in day-of-month still counts as unrestricted for this rule. The preview on this page implements exactly that behaviour, which is why an expression with both day fields set shows the OR explicitly in its description.

If you genuinely need "the first Monday of the month", cron cannot express it. Schedule it for every Monday and guard the command: 0 9 * * 1 [ "$(date +\%d)" -le 07 ] && /usr/local/bin/report.

Escaping, PATH, and the other crontab gotchas

A crontab line is not a shell script, and several of its rules exist nowhere else:

  • `%` is special. In the command portion, an unescaped % is converted to a newline, and everything after the first one is fed to the command on stdin. This is why date +%Y-%m-%d silently produces nothing useful in a crontab. Write date +\%Y-\%m-\%d, or put the command in a script file and call that instead — which is what you should be doing anyway.
  • PATH is minimal. cron typically runs with PATH=/usr/bin:/bin and nothing else. A command that works in your shell fails under cron because /usr/local/bin is missing. Use absolute paths for everything, or set PATH= explicitly at the top of the crontab.
  • The shell is `/bin/sh`, not your login shell. Bash-isms like [[ ]], source, and process substitution are not available unless you set SHELL=/bin/bash in the crontab.
  • Nothing from your login environment is loaded. No .bashrc, no .profile, no nvm, no rbenv, no virtualenv. If your job needs an interpreter version manager, invoke the absolute path to the real binary.
  • Every line must end with a newline. A crontab whose last line lacks one is rejected or silently ignored by some implementations.
  • A system crontab has a sixth field. /etc/crontab and files in /etc/cron.d insert a username between the schedule and the command. A user crontab (crontab -e) does not. Pasting one into the other is a very common cause of "bad command" errors.
# Set the environment explicitly — cron gives you almost nothing
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected]
TZ=UTC

# m  h  dom mon dow  command
  */5 *  *   *   *   /usr/local/bin/healthcheck.sh >> /var/log/healthcheck.log 2>&1
  30  2  *   *   *   /usr/bin/certbot renew --quiet
  0   4  *   *   0   /usr/local/bin/weekly-report.sh --date "$(date +\%F)"
  0   9  1   *   *   /usr/bin/php /var/www/app/artisan invoices:send
A crontab that actually works

The >> logfile 2>&1 on the first line is not decoration. By default cron mails any output to the MAILTO address, and if no MTA is configured that output is discarded entirely — which is exactly why "my cron job does nothing and there is no error" is such a common complaint. Redirect to a file you can read, and never redirect to /dev/null while you are still debugging.

Timezones and DST

cron uses the system timezone unless a TZ= line overrides it for subsequent entries. Most servers run UTC, so a job written as 0 9 * * * fires at 09:00 UTC, which is 04:00 in New York during winter. The preview on this page is computed in your browser's timezone, which is convenient for sanity-checking but is not necessarily the server's — check with timedatectl before trusting it for production.

Daylight saving is where correctness gets genuinely hard. On the spring-forward night a job scheduled inside the skipped hour never fires that day; on the autumn night a job in the repeated hour may fire twice. Vixie cron has heuristics for jobs scheduled less than once an hour, but they are heuristics. The robust answer is to run the server in UTC and let the application do timezone conversion, or to make the job idempotent so a double run is harmless.

Nicknames and @reboot

Most cron implementations accept shorthand in place of the five fields: @yearly (0 0 1 1 *), @monthly (0 0 1 * *), @weekly (0 0 * * 0), @daily / @midnight (0 0 * * *), and @hourly (0 * * * *). This tool expands all of them when you paste one. @reboot is different — it runs once when cron itself starts, has no clock schedule, and therefore has no "next run" to compute. It also does not fire on a systemctl reload cron, and in many container images it never fires at all because cron is started before the thing it depends on.

Be sceptical of six-field expressions. If you found one in documentation, it is probably Quartz (Java) or Spring, where the first field is *seconds* — pasting it into a Unix crontab shifts every field by one and produces a schedule nothing like what you wanted. systemd timers use a completely different syntax again (OnCalendar=), and crontab will reject it outright.

Verifying on the server

crontab -l                 # list the current user's crontab
crontab -e                 # edit it (syntax is validated on save)
sudo crontab -l -u www-data  # another user's crontab

# Did cron actually pick up and run the job?
grep CRON /var/log/syslog          # Debian/Ubuntu
journalctl -u cron --since today   # systemd
journalctl -u crond --since today  # RHEL/Fedora

# What timezone is the daemon actually in?
timedatectl

# Run the command exactly as cron will, with cron's minimal environment
env -i /bin/sh -c '/usr/local/bin/myjob.sh'
Checking that cron is really doing what you think

That last command is the fastest way to reproduce the classic "works in my shell, fails in cron" bug: env -i strips the environment down to nothing, which is much closer to what cron gives you than your interactive session is. If it fails there, the problem is PATH or a missing environment variable, not cron.

One more habit worth building: make jobs that must not overlap take a lock. flock -n /var/run/myjob.lock /usr/local/bin/myjob.sh exits immediately if a previous run is still going, which prevents the slow-job pile-up that turns a five-minute schedule into an outage.

chmod Calculator
Permission bits ↔ octal, with explanations

Frequently asked questions

What does */15 * * * * mean?
Every 15 minutes, at :00, :15, :30 and :45 past every hour of every day. The step counts from the start of the field's range (0), not from the moment you install the crontab, so the fire times are aligned to the clock.
Why does my job with both a day-of-month and a day-of-week run too often?
Because cron ORs the two day fields when both are restricted. 0 0 1 * MON fires on the 1st of the month *and* on every Monday. To get an AND, restrict one field in cron and test the other inside the command — for example schedule every Monday and add [ "$(date +\%d)" -le 07 ] to limit it to the first one.
Why does my date command produce nothing in cron?
An unescaped % in a crontab command is turned into a newline, and everything after the first one becomes stdin for the command. date +%F therefore never reaches date. Escape each one as \%, or move the command into a shell script — the script file has no such rule.
My cron job works when I run it manually but not from cron. Why?
Nine times out of ten it is PATH or a missing environment. cron runs with a near-empty environment and /bin/sh, and loads none of your dotfiles. Use absolute paths for every binary, set PATH= at the top of the crontab, and reproduce the failure with env -i /bin/sh -c 'yourcommand'.
What timezone does cron use?
The system timezone, unless a TZ= line earlier in the crontab overrides it. Check with timedatectl. The next-run preview on this page uses your browser's timezone, which is often not the same — treat it as a sanity check of the expression, not a prediction of server behaviour.
Is 0 and 7 really the same day?
Yes, both mean Sunday, which exists so that both the "week starts Sunday" and "week ends Sunday" conventions can be written naturally. 1-5 is therefore Monday to Friday, and 6,0 (or 6,7) is the weekend.
Why did my @reboot job not run?
It runs when the cron daemon starts, not on systemctl reload. If cron starts before the network, the filesystem mount, or the database it depends on, the job runs and fails silently. For anything with dependencies, a systemd unit with a proper After= is a far better fit than @reboot.
How do I stop two runs of the same job overlapping?
Wrap the command in flock -n: */5 * * * * flock -n /var/run/job.lock /usr/local/bin/job.sh. Without a lock, a job that occasionally takes longer than its interval will stack up copies of itself until the machine falls over.
Can cron run something more often than once a minute?
No — one minute is the finest resolution the five-field format can express. The usual workaround is a job that runs each minute and internally loops with sleep, but at that point a systemd timer with OnUnitActiveSec=10s or a long-running supervised process is the better design.