← Back to Blog

Cron Expression Deep Dive: Fields, Special Characters, Common Patterns & Troubleshooting

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

  1. Specifying both day and week: Standard Cron treats them as OR; Quartz requires ? on one
  2. Different week numbering: Standard Cron's 0 and 7 are both Sunday; Quartz's 1 is Sunday, 7 is Saturday
  3. 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 support 0-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:

Best Practices

  1. Specify timezone: Always document the timezone in config and docs
  2. Avoid boundary times: Don't use exact midnight (0 0 0 * * *) — multiple services firing simultaneously causes load spikes. Add a random offset
  3. Comment clearly: Add comments explaining each cron expression's trigger logic
  4. Design for idempotency: Task logic should be idempotent to prevent data issues from duplicate runs
  5. Set timeouts and retries: Prevent stuck tasks from blocking subsequent schedules
  6. Monitor and alert: Set up failure alerts for critical scheduled tasks
Advertisement

Frequently Asked Questions

What is the difference between 5, 6 and 7 field cron?

Standard Unix cron has **5 fields** (minute hour day-of-month month day-of-week). Quartz, in the Java ecosystem, uses **6 or 7**, adding **seconds** at the front and optionally **year** at the end. So `0 0 12 * * ?` means 12:00:00 daily in Quartz, but fed to a standard cron it parses into nonsense such as minute 0 of hour 12 with a shifted day field. **The two syntaxes are not compatible — verify every expression when migrating.** The [Cron parser](/cron-parser.html) shows the actual trigger times before you ship.

What does `*/5 * * * *` actually mean?

Run every 5 minutes. `*/n` means *every n units within that field's range*, triggered on **field boundaries**: `*/5` in the minute field fires at minutes 0, 5, 10 … 55, not five minutes from whenever the process started. Also note that when both the day-of-month and day-of-week fields hold specific values, the relationship is **OR** (execute if either matches), not AND — the single most misunderstood rule for newcomers.

My scheduled job never ran — what should I check first?

Check five things in order: (1) **time zone** — containers default to UTC, so the 09:00 you meant is 17:00; (2) **environment variables** — cron's environment differs from an interactive shell and `PATH` is often minimal, so use absolute paths; (3) **script permissions and line endings** — the `.sh` needs `chmod +x`, and files edited on Windows can throw `bad interpreter`; (4) **logging** — cron does not print to a terminal, so redirect output to a file explicitly; (5) **the expression itself** — verify the trigger times with a parser first.

How do I express the last day of the month?

Standard cron has no last-day syntax, so work around it: (1) list the candidate days — `0 0 28-31 * *` combined with a script check for whether tomorrow is the 1st; (2) use `@monthly`, which equals `0 0 1 * *` — the *first* day, not the last; (3) switch to a scheduler supporting the `L` token (Quartz does). **Option 1 is recommended** for portability across schedulers.

← Back to Blog