← 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

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.

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