Pricing Blog Compare Glossary
Login Start Free

Cron Expressions Explained: Complete Syntax Guide

Master cron expressions with detailed explanation of all 5 fields, special characters, step values, common patterns, timezone handling, and debugging tips.

2026-03-26 · 12 min · Technical Guide

A cron expression is five numbers that define when a job runs. 0 9 * * 1-5 means: 09:00 every weekday. It's the standard way to schedule periodic tasks in Unix and all cloud services.

The syntax looks simple at first, but the devil is in the details. The difference between 0 9 15 * ? (9:00 on the 15th) and 0 9 ? * 5 (9:00 every Friday) can cost hours of debugging. Misinterpreting timezone can cause a job to run one hour earlier or later due to daylight saving time. This guide covers all the corner cases.

The Five Fields of a Cron Expression

MIN HOUR DOM MON DOW

MIN: minute (0-59)

HOUR: hour (0-23, 24-hour format)

DOM: day of month (1-31)

MON: month (1-12, or JAN-DEC)

DOW: day of week (0-7, where 0=Sunday, 7=Sunday, 1=Monday)

Example: Parsing an Expression

30 14 15 6 *
│  │  │  │  └─ day of week: any (*) 
│  │  │  └──── month: June (6)
│  │  └─────── day of month: 15
│  └────────── hour: 14 (2:00 PM)
└───────────── minute: 30

Result: 14:30 (2:30 PM) on June 15 any year, any day of week

Key point: if day-of-month AND day-of-week are both filled (not *), they work with OR logic, not AND. The job runs if EITHER day-of-month matches OR day-of-week matches. This is a frequent source of errors.

Special Characters

* (asterisk) — any value

0 * * * * → every hour at the start (00:00, 01:00, 02:00, ...)

* * * * * → every minute

0 0 * * * → every day at midnight

/ (slash) — step/interval

*/5 * * * * → every 5 minutes (0, 5, 10, 15, ...)

0 */4 * * * → every 4 hours (00:00, 04:00, 08:00, ...)

0 0 1 */3 * → every third month on the 1st (January, April, July, ...)

0 0 * */6 * → every 6th month

- (dash) — range

0 9-17 * * * → every hour from 09:00 to 17:00 (business hours)

0 9 * 1-3 * → 09:00 every day from January to March

0 9 * * 1-5 → 09:00 Monday to Friday (weekdays)

, (comma) — list of values

0 9,12,15 * * * → at 09:00, 12:00, 15:00

0 0 1,15 * * → at midnight on the 1st and 15th

0 9 * * 1,3,5 → 09:00 on Monday, Wednesday, Friday

? (question mark) — no value

? is used ONLY in day-of-month OR day-of-week field (never both). It means: "no constraint in this field". You need this to avoid conflicts.

0 9 15 * ? → 09:00 on the 15th (day of week doesn't matter)

0 9 ? * 1 → 09:00 every Monday (day of month doesn't matter)

0 9 ? * 1-5 → 09:00 Monday to Friday

Common Examples

Daily Tasks

0 0 * * * → midnight every day

0 6 * * * → 06:00 every day (dawn, popular for backups)

0 12 * * * → noon every day

0 23 * * * → 23:00 every day

Weekly Tasks

0 9 ? * 1 → 09:00 every Monday

0 9 ? * 1-5 → 09:00 Monday to Friday (weekdays)

0 9 ? * 0 → 09:00 every Sunday

0 9 ? * 0,6 → 09:00 on weekends (Saturday and Sunday)

Monthly Tasks

0 0 1 * ? → midnight on the 1st of every month

0 0 15 * ? → midnight on the 15th of every month

0 0 L * ? → last day of month (some systems support L)

0 0 ? * 1L → last Monday of the month

Interval Tasks

*/5 * * * * → every 5 minutes

0 */6 * * * → every 6 hours (00:00, 06:00, 12:00, 18:00)

0 */4 * * * → every 4 hours

*/30 * * * * → every 30 minutes

15 * * * * → at the 15th minute of every hour

Business Hours

0 9-17 * * 1-5 → every hour from 09:00 to 17:59 on weekdays

0 */2 * * 1-5 → every 2 hours on weekdays

0 18 * * 1-5 → 18:00 (6 PM) on weekdays (end of business)

Shortcuts (@-syntax)

Some systems (Celery, APScheduler, Kubernetes) support shorthand notation:

@yearly or @annually = 0 0 1 1 * (January 1 at midnight)

@monthly = 0 0 1 * * (1st at midnight)

@weekly = 0 0 ? * 0 (Sunday at midnight)

@daily = 0 0 * * * (midnight)

@hourly = 0 * * * * (start of every hour)

@reboot = on system startup (crontab only, not in Celery)

Advanced Format: 6 Fields (with Seconds)

Standard cron has 5 fields (minute, hour, day, month, weekday). Some systems support 6-field format with seconds:

SEC MIN HOUR DOM MON DOW

Celery Beat (Python tasks)

Google Cloud Scheduler (doesn't support, only 5 fields)

Quartz Scheduler (Java, supports 6 fields)

Unix crontab (doesn't support, only 5 fields)

# 6-field format (Celery)
*/15 * * * * *  # every 15 seconds
0 */5 * * * *   # every 5 minutes

Timezone in Cron

Cron runs in a specific timezone. The problem: during daylight saving time shifts, clocks move 1 hour. If your job should run at 2:00 AM and the system jumps from 1:59 AM to 3:00 AM — your job never runs.

UTC vs Local Timezone

Unix crontab: uses local timezone of the server (from /etc/timezone)

Celery Beat: specify timezone explicitly in config

Google Cloud Scheduler: specify timezone explicitly (default: UTC)

AWS EventBridge: uses UTC by default

Kubernetes CronJob: uses UTC always

Correct Configuration in Celery

# celery.py
from celery import Celery
from celery.schedules import crontab

app = Celery('myapp')

# Specify timezone explicitly
app.conf.timezone = 'Europe/Moscow'  # or 'America/New_York', 'UTC'

app.conf.beat_schedule = {
    'daily-backup': {
        'task': 'backup.run',
        'schedule': crontab(hour=2, minute=0),
        # 2:00 AM Moscow time
    },
    'hourly-sync': {
        'task': 'sync.run',
        'schedule': crontab(minute=0),
        # Every hour at the start
    },
}

Always specify timezone explicitly. Don't rely on server defaults.

Common Cron Expression Mistakes

Mistake 1: Wrong day-of-week range

❌ Wrong: 0 9 * * 1-6 (interpreted as Mon-Sat)

❌ Wrong: 0 9 * * 0-5 (interpreted as Sun-Fri, not business days!)

✓ Correct: 0 9 * * 1-5 (Mon-Fri = weekdays)

Mistake 2: Both day and weekday fields filled

❌ Wrong: 0 9 15 * 1

This runs on the 15th OR Monday. If the 15th falls on Thursday, the job runs twice (once on the 15th, once on the following Monday).

✓ Correct: 0 9 15 * ? (exactly on the 15th)

✓ Correct: 0 9 ? * 1 (exactly on Monday)

Mistake 3: Forgot leading zero

❌ Wrong: 9 9 * * *

This means: 09:09 (9th minute of 9th hour), not 09:00

✓ Correct: 0 9 * * * (09:00, zero minute)

Mistake 4: Interval crossing boundary

❌ Wrong: 0 9-18/5 * * *

Systems disagree on interpretation. Usually this means: 09:00, 14:00 (doesn't include end-of-day hour 18).

✓ Correct: 0 9,14 * * * (explicitly list hours)

Mistake 5: Wrong month

❌ Wrong: 0 0 1 12 * (intended: December)

This works, but clarity is lacking. Better to use month names.

✓ Correct: 0 0 1 DEC * or 0 0 1 12 *

Debugging Cron Expressions

Online Validators

Before running cron in production — validate with an online validator. Enter the expression, see the next 10 runs.

crontab.guru — simple, instant feedback, supports 5-field format

Logging in Crontab

# Unix crontab
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=admin@example.com

# Outputs to /var/log/syslog
0 9 * * * /path/to/script.sh

# Or redirect to file
0 9 * * * /path/to/script.sh >> /var/log/my-cron.log 2>&1

Always log cron jobs. Silent failures are the most expensive. If a job didn't run — logs show what happened (permission denied, file not found, non-zero exit code).

Monitoring Cron Jobs with AtomPing

Cron jobs don't send heartbeats automatically. Use Heartbeat check in AtomPing. Your cron job should:

# Python cron task
def my_cron_job():
    try:
        # Do work
        result = expensive_operation()
        
        # Send heartbeat to AtomPing
        requests.post(
            'https://your-monitor.atomping.com/heartbeat/my-job',
            json={'status': 'success', 'result': result}
        )
    except Exception as e:
        # Send failure
        requests.post(
            'https://your-monitor.atomping.com/heartbeat/my-job',
            json={'status': 'failed', 'error': str(e)},
            headers={'X-Webhook-Status': 'error'}
        )

# In crontab:
0 9 * * * /usr/bin/python3 /path/to/cron_job.py

If the heartbeat doesn't arrive within the expected window (e.g., 65 seconds for a job scheduled at 9:00) — AtomPing alerts. This catches:

✓ Job didn't run (cron broken, script deleted)

✓ Job hung (processing takes 10+ minutes)

✓ Job crashed (unhandled exception)

Cron Expressions Checklist

Syntax: are the 5 fields filled correctly (min hour dom mon dow)?

Special characters: use *, -, /, , ? correctly

Day-of-month vs day-of-week: if both filled — use ? in one

Timezone: specify timezone explicitly (don't rely on default)

Validation: validate on crontab.guru before production

Logging: configure output to a log file

Monitoring: set up heartbeat check to track success

Related Articles

Cron Job Monitoring — how to monitor cron jobs with Heartbeat checks

Complete Guide to Uptime Monitoring — overview of monitoring

Heartbeat Check — in AtomPing

FAQ

What is a cron expression?

A cron expression is a string that specifies when a scheduled job should run. It uses 5 fields (minute hour day month weekday) to define the schedule in a compact format. For example: '0 9 * * 1-5' means every weekday at 9:00 AM. Cron expressions are used in Unix/Linux systems, cloud schedulers (AWS EventBridge, Google Cloud Scheduler), and background job frameworks (Celery, APScheduler, node-cron).

What do the 5 fields in cron mean?

Minute (0-59): which minute of the hour. Hour (0-23): which hour of the day (24-hour format). Day of Month (1-31): which day of the month. Month (1-12): which month of the year. Day of Week (0-7): which day of the week (0 and 7 are Sunday). The order matters: '30 14 15 6 *' = 2:30 PM on June 15th of any day of the week.

What do the special characters mean in cron?

* (asterisk) = any value in that field. , (comma) = multiple values (e.g., 1,3,5 = 1st, 3rd, 5th). - (dash) = range (e.g., 1-5 = 1 through 5). / (slash) = step/interval (e.g., */5 = every 5 units). ? (question mark) = no specific value (used only in day or weekday to avoid conflicts). Examples: */30 = every 30 minutes, 0-6 = 0 through 6, 9-17 = 9 AM to 5 PM.

What is the difference between day-of-month and day-of-week?

Both specify 'which days'. Day-of-month (3rd field) = calendar days (15, 16, 17). Day-of-week (5th field) = weekdays (Monday, Tuesday, etc.). If you specify both, cron uses OR logic: the job runs on EITHER the specified day-of-month OR the specified day-of-week. To avoid conflicts, use '?' in one field when the other is specified. '0 9 15 * ?' = 9 AM on the 15th day of the month only.

What's the difference between UTC and local timezone in cron?

Cron expressions are evaluated in a specific timezone. By default, cron uses the system's local timezone. In cloud schedulers (AWS, Google Cloud) and modern frameworks (Celery Beat, APScheduler), you specify the timezone explicitly. If you don't specify, assume UTC. This matters for daylight saving time: when clocks spring forward/back, a '9:00 AM' job might run at 8 AM or 10 AM local time. Always set timezone explicitly in production.

How do you monitor cron jobs with AtomPing?

Cron jobs don't send heartbeats automatically. Use the 'Heartbeat' check type: your cron job should POST to an AtomPing endpoint when it completes (including success/error status). If the heartbeat doesn't arrive within expected window (e.g., 65 seconds for a job that runs every 60 seconds), AtomPing alerts you. This detects failures (job didn't run), hangs (job ran but took too long), and crashes (job exited abnormally).

Start monitoring your infrastructure

Start Free View Pricing