Cron Expression Guide: From Basics to Advanced Scheduling
Cron expressions define when scheduled jobs run. They look cryptic at first — 0 /6 * 1-5 — but follow a simple structure. This guide covers everything from basics to edge cases that trip up experienced developers.
The five fields
A standard cron expression has five fields:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *Each field accepts:
- •A number:
5— matches exactly 5 - •A wildcard:
*— matches every value - •A range:
1-5— matches 1 through 5 - •A list:
1,3,5— matches 1, 3, and 5 - •A step:
*/15— matches every 15th value (0, 15, 30, 45)
20+ real-world examples
Every X minutes
# Every minute
* * * * *
# Every 5 minutes
*/5 * * * *
# Every 15 minutes
*/15 * * * *
# Every 30 minutes
*/30 * * * *Hourly
# Every hour (at minute 0)
0 * * * *
# Every 2 hours
0 */2 * * *
# Every 6 hours
0 */6 * * *
# Every hour from 9 AM to 5 PM
0 9-17 * * *Daily
# Every day at midnight
0 0 * * *
# Every day at 3:30 AM
30 3 * * *
# Every day at 6 AM and 6 PM
0 6,18 * * *
# Twice a day at 8 AM and 8 PM
0 8,20 * * *Weekly
# Every Monday at 9 AM
0 9 * * 1
# Every weekday at 8 AM
0 8 * * 1-5
# Every weekend at noon
0 12 * * 0,6
# Every Friday at 5 PM
0 17 * * 5Monthly
# First day of every month at midnight
0 0 1 * *
# 15th of every month at 3 AM
0 3 15 * *
# Last weekday of every month (approximate — use 28th)
0 0 28 * *
# Every quarter (Jan, Apr, Jul, Oct) on the 1st
0 0 1 1,4,7,10 *Advanced patterns
# Every 10 minutes during business hours on weekdays
*/10 9-17 * * 1-5
# Every 5 minutes on the first of each month
*/5 * 1 * *
# At 2:15 AM every day
15 2 * * *
# Every Sunday at 4 AM (weekly maintenance window)
0 4 * * 0Understanding step values
The / operator is the most misunderstood part of cron expressions.
*/15 in the minute field means "every 15 minutes starting from 0": minutes 0, 15, 30, 45.
But 5/15 means "every 15 minutes starting from 5": minutes 5, 20, 35, 50.
# Every 15 minutes: :00, :15, :30, :45
*/15 * * * *
# Every 15 minutes starting at :05: :05, :20, :35, :50
5/15 * * * *This is useful when you want to offset jobs to avoid running everything at the top of the hour.
Common gotchas
Gotcha 1: Timezone matters
Cron runs in the system's timezone (check with timedatectl or date +%Z). If your server is in UTC but you want a job at 9 AM Eastern:
# 9 AM ET = 2 PM UTC (during EST)
# 9 AM ET = 1 PM UTC (during EDT)
0 14 * * * # This is wrong half the year!Fix: Either set the system timezone to your target timezone, or use UTC and accept that the local time shifts with DST.
On systems with CRON_TZ support:
CRON_TZ=America/New_York
0 9 * * * # 9 AM Eastern, adjusts for DST automaticallyGotcha 2: DST transitions
During the spring-forward transition (e.g., 2 AM jumps to 3 AM):
- •A job scheduled for 2:30 AM is skipped — that time doesn't exist
- •A job scheduled for 3:00 AM runs normally
During the fall-back transition (e.g., 2 AM repeats):
- •A job scheduled for 1:30 AM may run twice — once before and once after the clock change
Fix: Schedule critical jobs outside the 1-3 AM window, or use UTC.
Gotcha 3: Day of month vs. day of week
When both day-of-month and day-of-week are specified (not *), cron runs the job when either condition is true, not both.
# This runs on the 15th AND every Monday — not "the 15th if it's a Monday"
0 0 15 * 1This is a cron specification quirk that trips up almost everyone. There's no standard way to say "the first Monday of the month" in a single cron expression.
Workaround: Use a wrapper script:
# Run every Monday, but only execute on the first Monday
0 9 1-7 * 1 # Runs on Mondays that fall on the 1st-7th
# Or use a script check
0 9 * * 1
# In your script:
[ $(date +%d) -le 7 ] || exit 0 # Skip if not the first weekGotcha 4: February and the 31st
# This runs only in months that have a 31st day
0 0 31 * * # Skips Feb, Apr, Jun, Sep, NovIf you want "last day of month" you need a script check:
# Run daily, check if tomorrow is the 1st
0 0 * * *
# In script:
[ "$(date -d tomorrow +%d)" = "01" ] || exit 0Gotcha 5: Leap years
# This runs Feb 29 only on leap years — silently skipped other years
0 0 29 2 *If you have a job that must run "at the end of February", use the tomorrow-is-first trick above.
Cron expression cheat sheet
| Expression | Description |
|---|---|
| `* * * * *` | Every minute |
| `*/5 * * * *` | Every 5 minutes |
| `0 * * * *` | Every hour |
| `0 0 * * *` | Every day at midnight |
| `0 0 * * 0` | Every Sunday at midnight |
| `0 0 1 * *` | First of every month |
| `0 0 1 1 *` | January 1st at midnight |
| `0 9-17 * * 1-5` | Every hour, 9-5, weekdays |
| `*/15 * * * *` | Every 15 minutes |
| `0 */6 * * *` | Every 6 hours |
| `30 4 * * *` | 4:30 AM daily |
| `0 0 15 * *` | 15th of each month |
Validating your expressions
Before deploying, test your cron expression:
# Using crontab guru (or similar tools)
# Expression: */15 9-17 * * 1-5
# "Every 15 minutes, 9 AM to 5 PM, Monday through Friday"When you set up monitoring in CronSafe, it parses your cron expression and shows you the human-readable schedule. If the description doesn't match your intent, you know the expression is wrong before deploying.
Monitoring your scheduled jobs
Whatever cron expression you use, add heartbeat monitoring. The expression defines when the job should run. CronSafe verifies that it actually ran. The combination of correct scheduling and active monitoring eliminates silent failures.
Start monitoring your cron jobs for free
20 monitors, email alerts, GitHub badges. No credit card required.
Get started free →