ENES
cronEngineering Guide

Cron Expressions: Fields, Special Characters, and Schedules You'll Actually Use (2026)

AS
Published on 2026-10-19Β·8 min readΒ·Daily Toolbox Engineering

Cron Expressions: Fields, Special Characters, and Schedules You'll Actually Use

Cron schedules half the internet's background work β€” backups, reports, cache warming, certificate renewals β€” and yet most developers write cron expressions by copying from Stack Overflow and hoping. The syntax is small enough to learn completely in one sitting: five fields, a handful of special characters, and three gotchas that explain nearly every "why did this run at the wrong time" mystery.

The Five Fields

A standard (Vixie/ISC) cron expression has five fields:

β”Œβ”€ minute (0-59)
β”‚ β”Œβ”€ hour (0-23)
β”‚ β”‚ β”Œβ”€ day of month (1-31)
β”‚ β”‚ β”‚ β”Œβ”€ month (1-12)
β”‚ β”‚ β”‚ β”‚ β”Œβ”€ day of week (0-7, where 0 and 7 are both Sunday)
β”‚ β”‚ β”‚ β”‚ β”‚
* * * * *

So 30 9 * * 1 means "at 09:30 on Mondays." Each field accepts a single value, a list (1,15), a range (1-5), a step (*/15), or * (every possible value).

Extended variants exist. Many systems (Quartz, GitHub Actions, AWS EventBridge) add fields:

  • 6 fields (seconds first): 0 30 9 * * 1 β€” used by Quartz and most "cron with seconds" implementations.
  • 7 fields (seconds + year): Quartz's full form.

Always check which dialect your scheduler speaks before copying an expression. A 5-field expression pasted into a 6-field scheduler silently means something different β€” usually "every second" instead of "every minute," which is how you DDoS yourself.

Special Characters

Char Meaning Example
* Every value * * * * * = every minute
, List 0 9,17 * * * = 9:00 and 17:00
- Range 0 9-17 * * 1-5 = hourly 9–17, Mon–Fri
/ Step */15 * * * * = every 15 minutes
L Last (Quartz) 0 0 L * * = last day of month
W Nearest weekday (Quartz) 0 0 15W * * = nearest weekday to the 15th
# Nth weekday (Quartz) 0 0 * * 2#1 = first Monday of month
? No specific value (Quartz) 0 0 12 * ? = noon daily, any weekday

L, W, #, and ? are Quartz extensions β€” standard Vixie cron does not understand them. If your scheduler is plain cron (most Linux crontab), stick to * , - /.

Steps deserve a closer look: */15 in the minute field means minutes 0, 15, 30, 45 β€” it steps from 0, not "every 15 minutes from now." And 5/15 means "starting at minute 5, every 15": 5, 20, 35, 50. Ranges with steps like 9-17/2 give 9, 11, 13, 15, 17.

The Day-of-Month vs Day-of-Week Trap

The single most misunderstood cron behavior: when both day-of-month and day-of-week are restricted (not *), cron treats them as OR, not AND.

0 0 15 * 3 does not mean "midnight on the 15th when it's a Wednesday." It means "midnight on the 15th of every month OR every Wednesday." This is POSIX-specified behavior, and it surprises everyone exactly once.

If you genuinely need the intersection ("the Wednesday that falls on the 15th" β€” rare), cron can't express it directly. Run on the broader condition and check the other inside the job:

# Run every Wednesday; exit unless it's also the 15th
0 0 * * 3 [ "$(date +\%d)" = "15" ] && /opt/job.sh

(Note the escaped \% β€” cron treats unescaped % as newline. Another classic gotcha: any % in a crontab command must be escaped as \%.)

Quartz sidesteps the ambiguity with ?: you must put ? in one of the two fields, making "which one is active" explicit.

Schedules You'll Actually Use

Expression Meaning
*/15 * * * * Every 15 minutes
0 * * * * Every hour, on the hour
0 2 * * * Daily at 02:00
0 9 * * 1-5 Weekdays at 09:00
0 0 * * 0 Weekly, Sunday midnight
0 0 1 * * Monthly, 1st at midnight
0 0 1 1 * Yearly, Jan 1 midnight
30 2 * * 6 Saturdays at 02:30
0 9-18 * * 1-5 Hourly 09:00–18:00 on weekdays
*/5 9-17 * * 1-5 Every 5 min during business hours

Prefer off-peak, non-round times for heavy jobs: 17 3 * * * instead of 0 3 * * *. Everyone's backup runs at 03:00 sharp; staggering by odd minutes spreads load on shared infrastructure (databases, NFS, APIs with rate limits).

Common Mistakes

1. Timezone confusion. Cron runs in the system timezone (usually UTC on servers) unless configured otherwise. "09:00" in your crontab is 09:00 server time β€” verify what that is. For user-facing schedules, convert explicitly or use a scheduler with timezone support (most modern ones: GitHub Actions uses UTC; AWS EventBridge lets you set a timezone).

2. DST double-run / skip. On fall-back day, 01:30 happens twice β€” a job at 30 1 * * * runs twice. On spring-forward day, 02:30 never happens β€” 30 2 * * * is skipped. Vixie cron's exact behavior here varies by implementation; if it matters, schedule in UTC.

3. Environment differences. Cron jobs run with a minimal environment: tiny PATH, no shell profile, no virtualenv. The classic failure: the script works in your terminal but fails in cron because python3 or docker isn't on cron's PATH. Fix: use absolute paths everywhere, or set PATH and required variables at the top of the crontab.

4. Overlapping runs. If a job takes longer than its interval, cron starts a second copy β€” two backups writing the same files, two report jobs double-sending emails. Use a lock (flock) to serialize:

*/5 * * * * /usr/bin/flock -n /tmp/job.lock /opt/job.sh

5. Silent failures. Cron emails output to the crontab owner if mail is configured β€” on most modern servers it isn't, so failures vanish. Always redirect output to a log: >> /var/log/job.log 2>&1, and alert on non-zero exit codes.

6. % in commands. Unescaped % becomes a newline in Vixie cron, and everything after it is fed to the command's stdin. date +%Y-%m-%d in a crontab silently breaks β€” write date +\%Y-\%m-\%d.

Debugging Walkthrough: "It Ran at the Wrong Time"

  1. Count the fields. Paste the expression into a cron explainer and confirm the dialect (5 vs 6 fields). A 6-field scheduler reading your 5-field expression shifts every field one position.
  2. Check the timezone. Log date from within the job itself β€” the scheduler's idea of "now" is what counts, not yours.
  3. Verify OR vs AND. If the schedule involves both day-of-month and day-of-week, remember it's OR. This explains most "it ran on days I didn't expect."
  4. Look for duplicates. The same job in two crontabs (user crontab and /etc/cron.d/) runs twice. crontab -l shows only yours.
  5. Check DST. An off-by-one-hour shift appearing overnight in spring or fall is the timezone transition until proven otherwise.

Try It

Cron math rewards experimentation. DailyToolbox's free cron parser runs entirely in your browser β€” type an expression and see the next run times in plain English, in your timezone.

πŸ‘‰ Parse a cron expression β†’

FAQ

What's the difference between cron and systemd timers?

Systemd timers are the modern Linux alternative: they support calendar expressions (OnCalendar=*-*-* 02:00:00), monotonic triggers (OnBootSec=15min), timezone settings, and built-in logging via the journal. Cron is simpler and universal; timers are more expressive and observable. For new work on systemd systems, timers are usually the better choice.

Why does 0 0 29 2 * not run every 4 years?

It does run β€” but only on Feb 29, which exists only in leap years. The subtlety: in non-leap years cron skips it silently rather than erroring. If you need "last day of February," standard cron can't express it; run on the 28th and check, or use a scheduler with L support.

Should I use cron inside Docker containers?

Generally no β€” one process per container is the Docker model, and cron daemons complicate signal handling and logging. Better: schedule outside (host cron, Kubernetes CronJobs, or your cloud scheduler) and let it start a container per run. If you must, run cron as PID 1 with cron -f and log to stdout.

How do I test a cron expression without waiting?

Use a parser (like the linked tool) to list the next N run times. For the job itself, run the command manually in an environment mimicking cron's: env -i PATH=/usr/bin:/bin /bin/sh -c 'your command' β€” this catches most environment-related surprises.

Is @daily the same as 0 0 * * *?

Yes β€” @daily (also @midnight) is shorthand for 0 0 * * *. Other shorthands: @hourly (0 * * * *), @weekly (0 0 * * 0), @monthly (0 0 1 * *), @yearly/@annually (0 0 1 1 *), and @reboot (run once at startup β€” handy, but remember it runs on every boot).

#cron#scheduling#devops#linux#automation
AS
Written by Alex Sun

Alex Sun is the developer behind Daily Toolbox. He writes these guides while building the tools themselves β€” every claim tested against the real thing.

Try the free tools mentioned above

Try the Cron Parser β†’