Every production system runs tasks on a schedule. Database backups at 3 AM, cache cleanup every hour, weekly analytics reports, health checks every 5 minutes. The language that describes all of these schedules is cron — a format invented in the 1970s that's still the universal standard today. When you see 0 */6 * * * in a GitHub Actions workflow or a Cloudflare Worker trigger, that's cron.
You don't need to memorize every combination. You need to read cron expressions when you encounter them and write basic ones when AI gets them wrong — because AI gets them wrong surprisingly often.
The 5-field format
Every cron expression has exactly 5 fields, separated by spaces, always in the same order:
┌───────── minute (0-59)
│ ┌─────── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌─── month (1-12)
│ │ │ │ ┌─ day of week (0-6, Sunday = 0)
│ │ │ │ │
* * * * *Read it left to right: minute, hour, day, month, weekday. That's the one thing to burn into memory. Everything else follows from understanding what each field accepts.
9 30 * * *, that's wrong — it means "at minute 9 of hour 30" which doesn't exist. The correct expression is 30 9 * * *.Field operators
Each field accepts more than just single numbers. Here are the four operators you'll see:
| Operator | Meaning | Example | Translates to |
|---|---|---|---|
* | Every possible value | * * * * * | Every minute of every hour |
, | List of values | 0,30 * * * * | At minute 0 and minute 30 |
- | Range | 0 9-17 * * * | Every hour from 9 AM to 5 PM, at minute 0 |
*/n | Every nth value | */15 * * * * | Every 15 minutes (0, 15, 30, 45) |
You can combine operators in a single field. 1-5 in the weekday field means Monday through Friday. 0,30 in the minute field means twice per hour at :00 and :30.
Reading examples
Let's decode a few real-world expressions:
0 * * * * → At minute 0 of every hour (hourly, on the hour)
30 2 * * * → At 2:30 AM every day
0 9 * * 1-5 → At 9:00 AM, Monday through Friday
*/10 * * * * → Every 10 minutes
0 0 1 * * → At midnight on the 1st of every month
0 6 * * 0 → At 6:00 AM every Sunday
15 14 1 * * → At 2:15 PM on the 1st of every monthThe trick to reading cron: start from the right. The rightmost non-* field tells you the biggest cycle. Then work left for the exact timing.
7 for Sunday alongside 0. GitHub Actions uses the standard 5-field format but runs in UTC. Always check your platform's docs for subtle differences.Common patterns you'll encounter
These are the cron expressions you'll see most often in codebases. Bookmark this table — it covers 90% of real-world use cases.
| Schedule | Expression | How to remember |
|---|---|---|
| Every minute | * * * * * | All stars = all the time |
| Every 5 minutes | */5 * * * * | Step on minute field |
| Every hour | 0 * * * * | Minute 0, every hour |
| Every day at midnight | 0 0 * * * | Minute 0, hour 0 |
| Every day at 9 AM | 0 9 * * * | Minute 0, hour 9 |
| Weekdays at 8:30 AM | 30 8 * * 1-5 | Mon-Fri range on weekday |
| Every Monday at 6 AM | 0 6 * * 1 | Weekday 1 = Monday |
| 1st of month at midnight | 0 0 1 * * | Day 1 |
| Every 6 hours | 0 */6 * * * | Step on hour field |
| Twice a day (9 AM, 9 PM) | 0 9,21 * * * | Comma list on hour |
Where cron is used in practice
You'll run into cron syntax in these contexts:
GitHub Actions
The schedule trigger uses cron directly:
on:
schedule:
- cron: '0 2 * * *' # Every day at 2 AM UTCCloudflare Workers (Cron Triggers)
[triggers]
crons = ["*/5 * * * *"] # Every 5 minutesLinux crontab
The original. Edit with crontab -e:
# Backup database every night at 3 AM
0 3 * * * /usr/local/bin/backup.sh
# Clean temp files every Sunday at midnight
0 0 * * 0 /usr/local/bin/cleanup.shNode.js (node-cron)
import cron from 'node-cron';
// Send weekly report every Monday at 9 AM
cron.schedule('0 9 * * 1', () => {
sendWeeklyReport();
});every('5 minutes') instead of cron syntax. These libraries add a dependency you probably don't need — most platforms already understand cron natively.Debugging cron schedules
When a scheduled job doesn't fire when expected, check these three things:
1. Timezone
Most cron systems run in UTC, not your local time. If your backup runs at "the wrong hour", it's probably correct in UTC.
# You want 9 AM Paris time (UTC+2 in summer)
# In UTC, that's 7 AM
0 7 * * *2. Field order mistakes
The number one cron bug: swapping minute and hour. Read your expression back out loud.
# WRONG: "At hour 30, minute 9" — hour 30 doesn't exist
9 30 * * *
# RIGHT: "At minute 30, hour 9" — 9:30 AM
30 9 * * *3. Day-of-month vs day-of-week conflicts
If you set both day-of-month AND day-of-week, most implementations run the job when either matches (OR logic), not when both match (AND logic). This is a classic source of "why does my job run on unexpected days?"
# Intended: "1st of the month, only if it's a Monday"
# Actual: "every 1st of the month AND every Monday"
0 0 1 * 1
# To get "1st Monday of the month", you need scripting logic,
# cron alone can't express thisQuick reference
| Field | Position | Range | Special values |
|---|---|---|---|
| Minute | 1st | 0-59 | */5 = every 5 min |
| Hour | 2nd | 0-23 | 9-17 = business hours |
| Day of month | 3rd | 1-31 | 1,15 = 1st and 15th |
| Month | 4th | 1-12 | 1-3 = Jan to Mar |
| Day of week | 5th | 0-6 | 1-5 = Mon to Fri |
| Symbol | Meaning | Example |
|---|---|---|
* | Every | * * * * * = every minute |
, | List | 0,30 = at 0 and 30 |
- | Range | 9-17 = 9 through 17 |
*/n | Step | */10 = every 10th value |