A cron expression is five space-separated fields — minute, hour, day-of-month, month, day-of-week — each specifying when a job runs. 0 9 * * 1-5 means "9:00 AM, Monday through Friday." The hard part isn't the syntax, it's the gotchas in day-of-month/day-of-week interaction and timezone handling that cause jobs to silently run at the wrong time.
The five fields
┌───────────── minute (0–59)
│ ┌───────────── hour (0–23)
│ │ ┌───────────── day of month (1–31)
│ │ │ ┌───────────── month (1–12)
│ │ │ │ ┌───────────── day of week (0–6, Sunday=0)
│ │ │ │ │
Each field accepts a value, a range, a list, a step, or a wildcard:
*— every value5— exactly that value1-5— a range (inclusive)1,3,5— a list*/15— every N units (step)
Common patterns worth memorizing
| Expression | Meaning |
|---|---|
| * * * * * | Every minute |
| 0 * * * * | Every hour, on the hour |
| 0 9 * * * | Every day at 9:00 AM |
| 0 9 * * 1-5 | Weekdays at 9:00 AM |
| */15 * * * * | Every 15 minutes |
| 0 0 1 * * | Midnight on the 1st of every month |
| 0 0 * * 0 | Every Sunday at midnight |
| 0 */6 * * * | Every 6 hours (00:00, 06:00, 12:00, 18:00) |
The gotcha almost everyone hits: day-of-month AND day-of-week
If you specify both day-of-month and day-of-week as non-wildcard values, most cron implementations treat them as OR, not AND. 0 0 15 * 5 doesn't mean "the 15th, if it's a Friday" — it means "midnight on the 15th, OR every Friday at midnight." This trips people up constantly when they want a schedule like "the first Friday of the month," which cron's five-field syntax genuinely can't express on its own — you need application logic on top, or a scheduler with extended syntax (L, #).
The other gotcha: whose timezone
Cron doesn't carry timezone information — it runs in whatever timezone the executing system (or cron daemon) is configured for. This is the single most common cause of "the job ran three hours late" bugs:
- A cron job on a server set to UTC running
0 9 * * *fires at 9:00 AM UTC, not 9:00 AM in your local time - Some platforms (Vercel Cron, GitHub Actions, most managed schedulers) explicitly run in UTC regardless of your system's local time
- Daylight saving transitions can cause a
0 2 * * *job to run twice, or not at all, on systems that use local time with DST
If a schedule needs to be "9 AM in New York" year-round, either configure the scheduler explicitly for that timezone if it supports it, or calculate the UTC offset yourself — and remember it changes twice a year with DST.
Day-of-week numbering isn't always consistent
Standard cron uses 0-6 for Sunday through Saturday, with 7 also accepted as Sunday in some implementations (Vixie cron, systemd timers). Not every scheduler agrees on this, so if a job seems to run on the wrong day, check whether the platform you're on treats 0 and 7 the same way — most do, but it's worth confirming rather than assuming.
Extended syntax (not standard, but common)
Many modern schedulers (Quartz, some Kubernetes CronJob-adjacent tools) support extensions beyond the POSIX five fields:
L— last day of the month/week (Lin day-of-month = last day of the month)#— nth weekday of the month (5#1= first Friday)?— "no specific value," used to avoid the day-of-month/day-of-week OR ambiguity in some implementations
These aren't portable across every cron implementation, so check your specific scheduler's docs before relying on them.
Debugging a schedule you didn't write
The fastest way to understand an unfamiliar cron expression — inherited from a teammate, found in a Kubernetes CronJob spec, buried in a CI config — is to decode it rather than mentally parse five cryptic fields. A Cron Expression Parser translates the expression into plain English and previews the next several run times, which also makes it easy to catch the day-of-month/day-of-week OR trap before it causes a production surprise.
Quick checklist
- Confirm which timezone your scheduler actually runs in — don't assume local time
- If you specify both day-of-month and day-of-week, remember it's OR, not AND
- Test with a preview/next-run-times tool before deploying a new schedule
- For "nth weekday of month" logic, check if your scheduler supports
#, or handle it in application code