Cron Expressions from Basics to Mastery: The Logic Behind 5 Fields and the Traps to Avoid
0 0 * * * vs * * * * 0 — which is "every day at midnight" and which is "every minute, but only on Sundays"?
Cron is daily bread for backend engineers, sysadmins, and data folks — yet the syntax is famously easy to get wrong. Swap the field order and your backup goes from daily to monthly. Worse, different platforms (Linux crontab, Quartz, Kubernetes CronJob, AWS EventBridge) all use slightly different dialects, so copy-pasting across systems is a minefield.
This article starts with the 5-field syntax, then covers dialects, common patterns, debugging techniques, and real war stories. Read it once and stop making cron typos.
5 Fields: The Heart of a Cron Expression
A standard Linux cron expression has 5 space-separated fields:
Memory trick: minute, hour, day, month, weekday — small to large, then weekday on the side.
| Field | Range | Special Values |
|---|---|---|
| Minute | 0-59 | — |
| Hour | 0-23 (24h) | — |
| Day of month | 1-31 | L = last day (some implementations) |
| Month | 1-12 | JAN-DEC also accepted |
| Day of week | 0-7 (both 0 and 7 = Sunday) | SUN-SAT also accepted |
Is Sunday 0 or 7? Standard Linux Cron accepts both. But in Quartz/Java (Spring Schedule) Sunday is 1, and the field order is different too — see the dialect comparison below.
5 Special Characters: Make Stiff Fields Flexible
Master these five and you can express 99% of scheduling needs.
1. Asterisk (*) — Any Value
2. Comma (,) — List Multiple Values
3. Hyphen (-) — Range
4. Slash (/) — Step (Interval)
5. Question Mark (?) — Don't Care (Quartz and Other Dialects Only)
Standard Linux Cron does not support ?, but Quartz / Spring / AWS use it to mean "this field doesn't matter", commonly to resolve the day vs day-of-week conflict (see below).
A Trap You Must Understand: How Day and Day-of-Week Relate
Every newcomer falls into this one:
You'd think it means "1st of the month and only if it's also a Sunday"? Wrong.
In Linux Cron, day and day-of-week are OR, not AND: satisfying either triggers the job. So the above is equivalent to:
Run at midnight on the 1st of every month PLUS at midnight every Sunday.
If you really want the AND ("Sunday that also falls in the first 7 days = first Sunday of the month"), you need a shell guard:
Or switch to a Quartz-style scheduler that supports ? — the logic is much cleaner there.
Why did Cron do this? Vixie Cron's design philosophy was "loose matching" — when given multiple constraints, prefer to run more often than skip. The behavior is counter-intuitive, but it's a historical artifact.
Common Patterns Cheat Sheet
Bookmark this table and copy-paste when writing cron.
| Goal | Expression | Notes |
|---|---|---|
| Every minute | * * * * * | Use sparingly, can spike resources |
| Every 5 minutes | */5 * * * * | — |
| Every hour on the hour | 0 * * * * | — |
| Every 2 hours | 0 */2 * * * | 0, 2, 4, ..., 22 |
| Daily at midnight | 0 0 * * * | — |
| Daily at 08:30 | 30 8 * * * | — |
| Daily at 08:00 and 20:00 | 0 8,20 * * * | — |
| Weekday business hours | 0 9-18 * * 1-5 | — |
| Monday at 07:00 | 0 7 * * 1 | 1 = Monday |
| Sunday at 22:00 | 0 22 * * 0 | 0 = Sunday |
| 1st of the month at midnight | 0 0 1 * * | — |
| Last day of the month | 0 0 28-31 * * + shell guard | Linux has no native L |
| First day of every quarter | 0 0 1 1,4,7,10 * | — |
| New Year's Day | 0 0 1 1 * | — |
| Every 15 seconds | (Not supported by Linux Cron) | Use systemd timer or Quartz |
Linux Shortcut Aliases
Dialect Comparison: Look Before You Copy
| System | Fields | Order | Sunday Index | Supports Seconds | Supports ? L W |
|---|---|---|---|---|---|
| Linux Vixie Cron | 5 | min hour day month weekday | 0=Sun | No | No |
| Quartz / Spring Schedule | 6 or 7 | sec min hour day month weekday [year] | 1=Sun | Yes | Yes |
| Kubernetes CronJob | 5 | min hour day month weekday | 0=Sun | No | No |
| AWS EventBridge | 6 | min hour day month weekday year | 1=Sun | No | Yes |
| GitHub Actions | 5 | min hour day month weekday | 0=Sun | No | No |
| Jenkins | 5 | min hour day month weekday | 0=Sun | No | Only H (hash) |
| node-cron / node-schedule | 5 or 6 | [sec] min hour day month weekday | 0=Sun | Yes | Partial |
The Three Most-Stepped-On Dialect Pitfalls
Pitfall 1: Quartz has an extra seconds field compared to Linux.
Spring @Scheduled(cron = "0 0 12 * * ?") corresponds to Linux 0 12 * * * — six fields vs five.
Pitfall 2: Quartz uses 1 for Sunday, Linux uses 0.
Linux "every Monday at midnight" is 0 0 * * 1. In Quartz it's 0 0 0 ? * 2 (6th field 2 is Monday; the day field must be ?).
Pitfall 3: Quartz forbids specifying both day and weekday.
Quartz requires that one of day or weekday be ?. 0 0 12 1 * 1 is invalid in Quartz — you must write either 0 0 12 1 * ? or 0 0 12 ? * 1.
Real-World Examples: 5 Production Scenarios
Scenario 1: Database Backup at 03:00
Key points:
- Pick predawn instead of midnight to avoid the rush
- Always redirect logs, otherwise cron failures vanish silently
2>&1captures stderr into the same file
Scenario 2: Send Daily Reports on Weekdays at 09:00
Note: holidays still trigger this. Handle inside the script if needed.
Scenario 3: Health Check Every 10 Minutes
-fsS makes curl exit non-zero on HTTP error, triggering the || fallback.
Scenario 4: Monthly Stats on the First Monday of Each Month
Linux Cron can't express "first Monday" directly. Because day-of-month and day-of-week are OR-combined, you can't simply write 1-7 * 1 (that fires on every day 1-7 plus every Monday). The reliable approach is a shell guard:
1-7: restrict the day field to the first week- Leave the weekday field as
*(critical — otherwise OR semantics fire too often) date +%ureturns ISO day-of-week (1=Monday); only run when it's 1
Exactly one day per month satisfies both "day in 1-7" and "Monday" — the first Monday.
Scenario 5: Yearly Tax Report (Runs Once a Year)
Avoid clock-aligned spikes: many scripts use 0 0 * * *, so every server fires the same second and causes a stampede. Offset to 5 0 * * * or 13 0 * * * to spread the load.
The 5 Most Common Pitfalls
1. Cron Doesn't See Your Environment Variables
Cron runs in a stripped-down environment — even $PATH is just /usr/bin:/bin. Scripts that rely on python, node, docker need absolute paths or an explicit export PATH=... at the top.
2. Percent Signs Must Be Escaped
In crontab files % is special — it gets replaced with a newline. Scripts using date +%Y-%m-%d need each % escaped:
3. Timezone Gotchas
Linux Cron uses the system timezone. Containers often default to UTC, so "8 AM daily" actually runs at 16:00 Beijing time.
CRON_TZ must appear at the top of the crontab file and is supported by only some implementations.
4. Job Concurrency Pile-Up
If your script takes 20 minutes but you set */10 * * * *, the second run starts before the first finishes. Use flock to enforce mutual exclusion:
-n means exit immediately if the lock isn't available — preventing pile-ups.
5. The Difference Between 2/30 and */30
*/30 * * * *→ minutes 0 and 30 (starts at 0, step 30)2/30 * * * *→ minutes 2 and 32 (starts at 2, step 30)
Many people assume */30 means 30, 60 — 0 is the starting point.
Debugging Techniques
Validate Before Going to Production
Don't just push to prod. Our Cron Parser shows the next few execution times for any expression — eyes miss errors that tools catch.
Make Sure Cron Is Actually Running
Use MAILTO to Email Errors
Any stderr from the script gets emailed to the address. Requires the mail command and a configured MTA on the system.
When NOT to Use Cron
Cron has barely evolved in decades. For these scenarios, reach for something else:
| Need | Better Option |
|---|---|
| Sub-second precision or complex schedules | systemd timer / Quartz |
| Execution history, retries, visualization | Airflow / Dagster / Prefect |
| Distributed scheduling with task dependencies | xxl-job / Argo Workflows |
| Scheduled jobs in a Kubernetes cluster | Kubernetes CronJob |
| Serverless / function triggers | AWS EventBridge / Cloud Scheduler |
| One-time delayed jobs | at command / delayed messages in a queue |
Wrap-Up
Cron looks simple but those five fields carry historical baggage:
- Field order: minute, hour, day, month, weekday — small to large then weekday
- Day and day-of-week are OR, not AND — the biggest Linux Cron gotcha
- Check the dialect before copying: Quartz adds a seconds field and starts Sunday at 1
- Validate every expression with a tool — eyeballing cron is error-prone
- Env, timezone, escaping: a script that runs in your shell may still fail under cron
Pair this article with the Cron Parser — punch in your expression, eyeball the next few firing times, and you've eliminated 90% of cron mistakes before they ship.