What is a Cron Expression?
A cron expression is a string that defines when a scheduled task should run. Originally from the Unix cron daemon, it's now used across scheduling systems — from Linux crontab to Spring's @Scheduled, Quartz Scheduler, Kubernetes CronJob, and more.
A cron expression consists of 5 to 7 fields, each representing a time unit.
Field Structure
Standard 5-Field Cron
* * * * *
│ │ │ │ │
│ │ │ │ └── Day of week (0-7, both 0 and 7 = Sunday)
│ │ │ └──── Month (1-12)
│ │ └────── Day of month (1-31)
│ └──────── Hour (0-23)
└─────────── Minute (0-59)
Quartz 6/7-Field Cron
Quartz Scheduler uses 6 or 7 fields, adding seconds and an optional year:
* * * * * * *
│ │ │ │ │ │ │
│ │ │ │ │ │ └── Year (optional, 1970-2099)
│ │ │ │ │ └──── Day of week (1-7, 1=Sunday)
│ │ │ │ └────── Month (1-12)
│ │ │ └──────── Day of month (1-31)
│ │ └────────── Hour (0-23)
│ └──────────── Minute (0-59)
└─────────────── Second (0-59)
Field Reference Table
| Field | Standard Cron Range | Quartz Range | Allowed Special Chars |
|---|---|---|---|
| Second | — | 0-59 | , - * / |
| Minute | 0-59 | 0-59 | , - * / |
| Hour | 0-23 | 0-23 | , - * / |
| Day | 1-31 | 1-31 | , - * ? L W C |
| Month | 1-12 or JAN-DEC | 1-12 or JAN-DEC | , - * / |
| Day of week | 0-7 or SUN-SAT | 1-7 or SUN-SAT | , - * ? L C # |
| Year | — | 1970-2099 | , - * / |
Special Characters Explained
* — Any Value
Matches all valid values for the field. * * * * * means every minute.
, — List
Specifies multiple discrete values:
0 0 9,12,18 * * ? // Run at 9:00, 12:00, and 18:00 daily
- — Range
Specifies a continuous interval:
0 0 9-17 * * ? // Run every hour from 9:00 to 17:00
/ — Step
start/step — starting from start, execute every step:
0 */15 * * * ? // Every 15 minutes
0 0/30 * * * ? // At 0 and 30 minutes past the hour
? — No Specific Value (Quartz only)
Used only in day-of-month and day-of-week fields. Since these two fields can conflict, Quartz requires one to be ? when the other is specified.
0 0 12 ? * MON // Every Monday at noon (day field is ?)
0 0 12 1 * ? // 1st of every month at noon (week field is ?)
L — Last (Quartz only)
- In day field: last day of the month
- In week field: last specified weekday of the month
0 0 23 L * ? // Last day of month at 23:00
0 0 10 ? * 6L // Last Friday of month at 10:00
W — Nearest Weekday (Quartz only)
In the day field, finds the nearest weekday (Mon-Fri) to the given date:
0 0 9 15W * ? // Nearest weekday to the 15th at 9:00
If the 15th is Saturday, runs on the 14th (Friday); if Sunday, runs on the 16th (Monday).
# — Nth Day of Week (Quartz only)
dayOfWeek#nth:
0 0 10 ? * 2#1 // First Monday of the month at 10:00
0 0 10 ? * 6#3 // Third Friday of the month at 10:00
Common Scheduling Patterns
High-Frequency Tasks
| Expression | Description |
|---|---|
* * * * * |
Every minute |
*/5 * * * * |
Every 5 minutes |
0 * * * * |
Every hour on the hour |
0 */2 * * * |
Every 2 hours |
Daily Tasks
| Expression | Description |
|---|---|
0 0 * * * |
Every day at midnight |
0 9 * * * |
Every day at 9 AM |
0 9 * * 1-5 |
Weekdays at 9 AM |
0 22 * * 1-5 |
Weekdays at 10 PM |
Periodic Tasks
| Expression | Description |
|---|---|
0 0 1 * * |
1st of every month |
0 0 1 1 * |
January 1st every year |
0 0 * * 0 |
Every Sunday |
0 0 1 */3 * |
First day of each quarter |
Quartz-Specific Patterns
| Expression | Description |
|---|---|
0 0 12 ? * MON-FRI |
Weekdays at noon (Quartz format) |
0 0 23 L * ? |
Last day of month at 23:00 |
0 0 9 15W * ? |
Nearest weekday to 15th at 9:00 |
0 0 10 ? * 6#3 |
Third Friday at 10:00 |
0 30 10-14 ? * MON,WED,FRI |
Mon/Wed/Fri, 10:30-14:30 hourly |
Quartz vs Standard Cron: Key Differences
| Feature | Standard Cron | Quartz |
|---|---|---|
| Field count | 5 | 6 or 7 |
| Second precision | ❌ | ✅ |
| Year field | ❌ | ✅ (optional) |
? character |
❌ | ✅ (required for day/week) |
L character |
❌ | ✅ |
W character |
❌ | ✅ |
# character |
❌ | ✅ |
| Week encoding | 0=Sun, 7=Sun | 1=Sun, 7=Sat |
| Day & week relationship | OR (either matches) | One must be ? |
Common Pitfalls
- Specifying both day and week: Standard Cron treats them as OR; Quartz requires
?on one - Different week numbering: Standard Cron's
0and7are both Sunday; Quartz's1is Sunday,7is Saturday - Seconds field: Forgetting to add it when migrating from standard to Quartz
Troubleshooting Guide
1. Expression Not Firing
Checklist:
- Is the timezone correct? Quartz defaults to JVM timezone
- Are day and week fields conflicting? Quartz requires one to be
? - Is step syntax correct? Use
*/15, not all implementations support0-59/15 - Is the date valid?
0 0 0 31 2 *never fires (Feb has no 31st)
2. Unexpected Frequency
# Intent: every 5 minutes
*/5 * * * * # ✅ Correct: 0, 5, 10, 15...
0/5 * * * * # ⚠️ Equivalent in some systems, not all
0-59/5 * * * * # ✅ Explicit range with step
3. Timezone Issues
- Linux cron uses the server's local timezone
- Quartz can set timezone via
CronTrigger - Kubernetes CronJob uses UTC — calculate accordingly
// Quartz with explicit timezone
CronTrigger trigger = TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder
.cronSchedule("0 0 9 * * ?")
.inTimeZone(TimeZone.getTimeZone("America/New_York")))
.build();
4. February 29th Problem
0 0 0 29 2 * only fires on leap years. For annual tasks, use program logic instead of relying on cron.
5. Daylight Saving Time
In DST-observing timezones, certain times are skipped or repeated during transitions. Recommendations:
- Avoid scheduling critical tasks between 2:00-3:00 AM
- Use UTC for scheduling
Scheduling Libraries by Language
| Language / platform | Common library | Notes |
|---|---|---|
| Node.js | node-cron, cron |
5/6 fields; no L/W/# support |
| Python | APScheduler, croniter |
croniter evaluates expressions only, it does not schedule |
| Java | Quartz, spring-task |
Quartz supports 6/7 fields and L/W/# |
| Go | robfig/cron |
5 fields by default; seconds optional |
| Kubernetes | CronJob |
Standard 5 fields, always UTC |
Extra Care in Distributed Environments
Standard cron only carries single-machine semantics. With multiple instances deployed, the same expression runs on every machine:
- Use a distributed lock (Redis Lock, or a unique DB index) so only one instance wins the right to execute
- Or move to a scheduler with cluster coordination (Quartz Cluster, xxl-job, or K8s CronJob with
concurrencyPolicy: Forbid) - Still keep the task idempotent — a lock only reduces the chance of concurrency, it does not replace idempotent design
Online Validation Tools
Validate before deploying:
- crontab.guru — Standard 5-field cron
- CronMaker — Quartz 7-field cron
- FreeFormatter Cron Validator
Best Practices
- Specify timezone: Always document the timezone in config and docs
- Avoid boundary times: Don't use exact midnight (
0 0 0 * * *) — multiple services firing simultaneously causes load spikes. Add a random offset - Comment clearly: Add comments explaining each cron expression's trigger logic
- Design for idempotency: Task logic should be idempotent to prevent data issues from duplicate runs
- Set timeouts and retries: Prevent stuck tasks from blocking subsequent schedules
- Monitor and alert: Set up failure alerts for critical scheduled tasks
Reproducible Results
Verify an expression in two steps before shipping it:
crontab -l # list the user's jobs (confirm it was actually written)
grep CRON /var/log/syslog # Debian/Ubuntu: read the cron daemon's execution log
0 9 * * 1-5 means "weekdays at 09:00". Paste the expression into the cron parser to see the field breakdown and the next fire time instantly. When debugging "it never ran": confirm the parse matches your intent first, then look for an execution record in the log at that timestamp. If both look right but nothing happened, the usual culprit is the environment — cron's PATH is only /usr/bin:/bin.