Cron expressions look like line noise the first time you see them. 0 9 1-5 -- what does that even mean? But the syntax is surprisingly simple once you learn the five fields. This guide will get you from confusion to confidence in about five minutes.
The five fields
A cron expression has five fields, separated by spaces:
┌───────────── 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 specific value:
5 - A wildcard:
*(every possible value) - A range:
1-5 - A list:
1,3,5 - A step:
*/5(every 5th value)
That's it. Five fields, five operators. Everything else is just combinations.
Reading cron expressions
Read them left to right: "At minute X, of hour Y, on day Z..."
| Expression | Meaning |
* | Every minute |
0 | Every hour (at minute 0) |
0 0 * | Every day at midnight |
0 9 * | Every day at 9:00 AM |
0 9 1-5 | Every weekday at 9:00 AM |
0 0 1 | First day of every month at midnight |
0 0 1 1 * | January 1st at midnight (once per year) |
Common patterns with examples
Every N minutes
# Every 5 minutes
/5 *Every 15 minutes
/15 *Every 30 minutes
/30 *
equivalent to:
0,30
Specific times daily
# 9:00 AM every day
0 9 *9:30 AM every day
30 9 *6:00 PM every day
0 18 *Twice daily: 9 AM and 6 PM
0 9,18 *
Business hours
# Every weekday at 9 AM
0 9 1-5Every hour during business hours, weekdays
0 9-17 1-5Every 30 minutes during business hours, weekdays
0,30 9-17 1-5
Weekly
# Every Monday at 9 AM
0 9 1Every Sunday at midnight
0 0 0Every Friday at 5 PM
0 17 5Tuesday and Thursday at 10 AM
0 10 2,4
Monthly
# First of every month at midnight
0 0 1 15th of every month at noon
0 12 15 Last day of month (not directly supported - use alternatives below)
This runs on 28th, which isn't always the last day:
0 0 28
The "last day of month" problem is a known limitation of cron. Most cron implementations don't have a "last day" keyword. Workarounds include running a daily job that checks the date:
# Run at midnight every day, but the script checks if tomorrow is the 1st
0 0 * [ "$(date -d tomorrow +\%d)" = "01" ] && /path/to/script.sh
Quarterly
# First day of each quarter at 9 AM
0 9 1 1,4,7,10 *
Special characters in detail
The step operator (/)
*/N means "every Nth value, starting from the beginning of the range."
# Every 5 minutes: 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55
/5 *Every 3 hours: 0, 3, 6, 9, 12, 15, 18, 21
0 /3 Starting at minute 5, then every 10 minutes: 5, 15, 25, 35, 45, 55
5/10
The range operator (-)
# Weekdays only (Monday through Friday)
0 9 1-5First 10 days of the month
0 0 1-10 Between 9 AM and 5 PM
0 9-17 *
The list operator (,)
# Specific minutes: 0, 15, 30, 45
0,15,30,45 Specific months: Jan, Apr, Jul, Oct
0 0 1 1,4,7,10 *Monday, Wednesday, Friday
0 9 1,3,5
Setting up cron jobs
On Linux/macOS
# Edit your crontab
crontab -eAdd a job (runs at 9 AM daily)
0 9 * /path/to/script.shList current jobs
crontab -lRemove all jobs (careful!)
crontab -r
Important notes:
- Cron jobs run in a minimal environment. Always use full paths for commands and files.
- Set the
PATHat the top of your crontab:PATH=/usr/local/bin:/usr/bin:/bin - Redirect output to a log file:
0 9 * /path/to/script.sh >> /var/log/myjob.log 2>&1 - Cron uses the system timezone by default. Set
CRON_TZ=UTCfor explicit timezone control.
Common debugging steps
If your cron job isn't running:
- Check the system cron log:
grep CRON /var/log/syslog - Verify the crontab is saved:
crontab -l - Test the command manually with the full path
- Check file permissions (the script must be executable)
- Make sure environment variables are set in the crontab, not just your shell
Cron in the cloud
Traditional cron runs on a single server. If that server goes down, your job doesn't run. Cloud schedulers solve this with managed, distributed execution.
AWS EventBridge (formerly CloudWatch Events)
# AWS uses a 6-field format (adds year) with "rate" expressions too
cron(0 9 ? *) # Daily at 9 AM UTC
rate(5 minutes) # Every 5 minutes
Note the ? in the day-of-week field -- AWS requires either day-of-month or day-of-week to be ? (not both *).
Google Cloud Scheduler
# Standard cron syntax
gcloud scheduler jobs create http my-job \
--schedule="0 9 *" \
--uri="https://my-api.com/run" \
--time-zone="America/New_York"
Google Cloud Scheduler supports standard 5-field cron with explicit timezone, which is a significant improvement over system cron.
GitHub Actions
on:
schedule:
- cron: '0 9 1-5' # Weekdays at 9 AM UTC
GitHub Actions cron is always UTC and has a minimum interval of 5 minutes. Jobs may run late during high-load periods.
Building and validating cron expressions
When you're constructing a complex cron expression, it helps to validate it before deploying. devdash.io/tools/cron-builder provides a visual builder where you can set each field and see the next 10 execution times. Useful for catching off-by-one errors before they hit production.
For command-line validation:
# Python - croniter library
pip install croniter
python3 -c "
from croniter import croniter
from datetime import datetime
cron = croniter('0 9 1-5', datetime.now())
for _ in range(5):
print(cron.get_next(datetime))
"
Quick reference
| I want to run... | Expression |
| Every minute | * |
| Every 5 minutes | /5 * |
| Every hour | 0 |
| Every day at midnight | 0 0 * |
| Every day at 9 AM | 0 9 * |
| Weekdays at 9 AM | 0 9 1-5 |
| Every Monday at 9 AM | 0 9 1 |
| First of every month | 0 0 1 |
| Every 6 hours | 0 /6 |
| Every quarter (Jan, Apr, Jul, Oct) | 0 0 1 1,4,7,10 * |
The syntax is simple. The hard part is remembering the field order. Minute, hour, day-of-month, month, day-of-week. Once that's muscle memory, you'll read cron expressions as naturally as regular expressions. Well, more naturally.