OneWebDesk

Cron Expressions Explained: Mastering the Five Fields

What each of the five crontab fields means, the patterns you actually use, and the timezone/DST traps.

A cron expression is the standard way to say “run this on a schedule” — the same five-field syntax powers Linux crontab, GitHub Actions, Kubernetes CronJobs, Jenkins, and countless SaaS schedulers. A single line like 30 2 * * 1-5means “every weekday at 02:30”, but until you know the field order it reads like line noise. This guide walks through the five fields, the four operators (* , - /), a copy-paste table of common patterns, and the two traps that cause real outages: the day-of-month/day-of-week OR rule and timezone/DST surprises.

Whatever you write, verify it before it ships. Paste the expression into the cron next-run calculatorand check that the list of upcoming fire times matches what you had in mind — the gap between “what I meant” and “what cron parsed” is where most scheduling bugs live.

The five fields: minute, hour, day, month, weekday

A standard crontab expression is five space-separated fields, read left to right:

PositionFieldAllowed valuesNotes
1Minute0-59 
2Hour0-2324-hour clock
3Day of month1-31 
4Month1-12Names jan-dec accepted
5Day of week0-7Both 0 and 7 mean Sunday; names sun-sat accepted

Inside each field you can use four operators:

  • * — every value. A * in the minute field means “every minute”.
  • , — a list. 0,30 fires at minute 0 and minute 30.
  • - — a range. 1-5 covers 1 through 5 inclusive (Monday to Friday in the weekday field).
  • / — a step. */5 means 0, 5, 10, … and combines with ranges, e.g. 10-50/10.

One subtlety: */Nmeans “values divisible by N counting from the start of the range”, not “N minutes after the last run”. Put */7 in the minute field and it fires at 0, 7, 14, …, 56, then resets to 0 at the top of the next hour — so the 56→0 gap is only four minutes. If you need a true fixed interval, cron alone cannot express it; reach for a systemd timer instead.

Worked example: decoding 30 2 * * 1-5

Take one of the most common production schedules, 30 2 * * 1-5, and read it field by field:

  1. Minute = 30 → at minute 30.
  2. Hour = 2 → only during hour 2. Combined: 02:30.
  3. Day of month = * → any day of the month.
  4. Month = * → any month.
  5. Day of week = 1-5 → Monday (1) through Friday (5).

Put together: “every weekday at 02:30” — the classic slot for backups and batch jobs that should run off-peak but skip weekends. Always read in the fixed order minute → hour → day → month → weekday and any expression decodes the same way.

Patterns you will actually use

ExpressionMeaning
*/5 * * * *Every 5 minutes (:00, :05, :10, …)
0 * * * *Hourly, on the hour
0 3 * * *Daily at 03:00
0 9 * * 1-5Weekdays at 09:00
0 0 1 * *Midnight on the 1st of every month
0 */6 * * *Every 6 hours (00:00, 06:00, 12:00, 18:00)
30 2 * * 0Sundays at 02:30

Crontab also accepts shortcuts: @hourly (= 0 * * * *), @daily (= 0 0 * * *), @weekly, @monthly, and @yearly are plain aliases, while @rebootis special — it fires once right after the system boots rather than at a clock time. Handy for cache warm-up or restarting a daemon, but not every cron implementation supports it (some containerized environments don't), so check before relying on it.

The big gotcha: day-of-month and day-of-week combine with OR

Here is the rule that trips up nearly everyone. If both the day-of-month field and the day-of-week field are restricted (neither is *), the job fires when either one matches — OR, not AND. That behavior is written into the POSIX crontab specification.

So 0 0 13 * 5 looks like “midnight on Friday the 13th” but actually fires on every 13th of the month and every Friday— four or five times a month. Likewise, “first Monday of the month” is not 0 9 1-7 * 1 (that runs on days 1–7 and every Monday). The standard fix is to keep only the weekday in the expression and add a date guard at the top of the script, e.g. [ $(date +\%d) -le 7 ] || exit 0.

These combined expressions are exactly the ones your eyes cannot verify, so run anything that restricts both fields through the cron next-run calculator and inspect the upcoming fire times before deploying.

Timezones and DST: stay away from the 2 a.m. hour

Crontab evaluates expressions in the system (or daemon) timezone. On a UTC server, 0 3 * * * runs at 03:00 UTC — which is noon in Seoul and 11 p.m. in New York. The same line fires at different wall-clock times on differently configured servers. The sanest setup for a fleet is to keep every server on UTC and convert timestamps when reading logs — the log timestamp converter turns UTC log lines into your local time in one paste.

Daylight saving time adds a nastier wrinkle. On spring-forward night the clock jumps from 02:00 straight to 03:00, so 02:30 never exists and a 30 2 * * * job may simply not run that day. On fall-back night 02:30 happens twice, so the job may run twice (Vixie cron has compensation logic; many other implementations do not). Three defenses:

  • Run servers in UTC — UTC has no DST, so the problem disappears.
  • If you must schedule in a DST timezone, avoid the 01:00–03:59 window.
  • Make jobs idempotent or use a lock file so a duplicate run is harmless.

Environment quirks: PATH and the % character

The number one cause of “works in my terminal, fails in cron” is the environment. Cron does not run a login shell, never reads your .bashrc, and ships a minimal PATH like /usr/bin:/bin — which is why docker, node, or awsdie with “command not found”. Fix it either by declaring PATH=/usr/local/bin:/usr/bin:/bin at the top of the crontab or by using absolute paths for every binary.

Second quirk: in a crontab command line, % is special. Everything after the first unescaped % is fed to the command as standard input, and each % becomes a newline — so an innocent date +%F silently breaks. Escape it as date +\%F. Finally, never throw output away: append >> /var/log/myjob.log 2>&1 so you can actually see why a job failed.

Frequently asked questions

What is the difference between 0 and 7 in the day-of-week field?
Nothing — both mean Sunday. POSIX defines 0-6 with 0 as Sunday, and most implementations additionally accept 7 as Sunday. For maximum portability, stick to 0 or the name sun.
What happens if I set both day-of-month and day-of-week?
They combine with OR, not AND. 0 0 13 * 5 fires on every 13th of the month AND on every Friday — not only on Friday the 13th. If you need both conditions to hold, keep one in the expression and check the other inside the script itself.
Why do some cron expressions have 6 or 7 fields?
Standard crontab uses 5 fields, but Quartz (Java) and some cloud schedulers prepend a seconds field (6 fields) and may append a year field (7 fields). Check your system's documentation first and never paste a 5-field expression into a 6-field scheduler unchanged.
Does */5 mean 'five minutes after the previous run finished'?
No. */5 selects clock values divisible by 5 counting from the start of the field's range — minutes 0, 5, 10, and so on. It is anchored to the wall clock, so even if a run takes 3 minutes, the next one still starts at the fixed minute.
Why does my script work in the terminal but fail under cron?
Almost always the environment: cron skips .bashrc so PATH is minimal, the working directory may not be your home, and an unescaped % in the command line is treated as a newline. Use absolute paths, declare PATH at the top of the crontab, escape % as \%, and redirect output to a log file.
Is a 02:30 cron job risky on a server with daylight saving time?
Yes. On spring-forward night 02:30 does not exist, so the job can be skipped; on fall-back night it occurs twice, so the job may run twice. Run servers in UTC (which has no DST) or avoid scheduling between 01:00 and 03:59.

Tools to use with this guide

Related guides