Cron Expressions Explained: From '* * * * *' to Complex Schedules
Cron expressions look intimidating—five fields of asterisks, slashes, and numbers that somehow encode "run every Tuesday at 3:30 AM." But once you understand the structure, reading and writing cron schedules becomes straightforward.
This guide breaks down cron expression syntax from the ground up, covers the patterns you'll actually use in production, and highlights the one gotcha that catches almost everyone. Try any expression live with our free Cron Parser as you read.
The Five Fields
A standard cron expression has five space-separated fields:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12 or JAN-DEC)
│ │ │ │ ┌───────────── day of week (0-7 or SUN-SAT)
│ │ │ │ │
* * * * *
Each field accepts:
| Syntax | Meaning | Example |
|---|---|---|
* | Every value | * * * * * = every minute |
5 | Specific value | 5 * * * * = at minute 5 |
1,15,30 | List | 0 9,17 * * * = 9 AM and 5 PM |
1-5 | Range | 0 9 * * 1-5 = Mon through Fri |
*/5 | Step | */5 * * * * = every 5 minutes |
10-30/5 | Range + step | 10-30/5 * * * * = at minutes 10, 15, 20, 25, 30 |
Note: Day of week uses 0-7 where both 0 and 7 mean Sunday. You can also use three-letter names: SUN, MON, TUE, WED, THU, FRI, SAT. Months accept JAN–DEC the same way. The Cron Parser expands these aliases to numbers and normalizes 7 to Sunday 0.
Name aliases: readable to humans, accepted by machines
People often copy 0 9 * * MON-FRI from docs and paste it into a digits-only validator that rejects it. Vixie crontab and most Linux distros accept three-letter English names (case-insensitive):
| Field | Alias example | Numeric equivalent |
|---|---|---|
| Month | JAN,JUN / JAN-MAR | 1,6 / 1-3 |
| Weekday | SUN / MON-FRI | 0 / 1-5 |
Aliases are pure sugar — same semantics as digits. When half the team writes numbers and half writes names, expand both in the parser and compare value lists instead of arguing whether Monday is 0 or 1.
Common Patterns You'll Actually Use
Here are the cron schedule examples that cover 90% of real-world needs:
Every 5 minutes:
*/5 * * * *
The step value */5 means "starting at 0, every 5th value." This fires at :00, :05, :10, :15, and so on.
Every weekday at 9:00 AM:
0 9 * * 1-5
Minute 0, hour 9, any day of month, any month, Monday through Friday.
First day of every month at midnight:
0 0 1 * *
Minute 0, hour 0, day 1, any month, any day of week.
Every Sunday at 2:30 AM:
30 2 * * 0
Every 15 minutes during business hours (9 AM to 5 PM), weekdays only:
*/15 9-17 * * 1-5
This fires at :00, :15, :30, :45 for each hour from 9 through 17, Monday to Friday. Note that it also fires at 17:15, 17:30, and 17:45—if you want to stop exactly at 17:00, you'd need two expressions or a more specific hour range like 9-16 plus 0 17 * * 1-5.
Every hour at the 30-minute mark:
30 * * * *
Twice daily (6 AM and 6 PM):
0 6,18 * * *
Every Monday, Wednesday, and Friday at 8 AM:
0 8 * * 1,3,5
The Day-of-Month vs Day-of-Week Gotcha
This is the one that bites people in production. When you specify both the day-of-month field and the day-of-week field (i.e., neither is *), cron uses OR logic, not AND.
0 0 1 * 5
You might read this as "run at midnight on the 1st of the month, but only if it's a Friday." That's wrong. It actually means: "run at midnight on the 1st of the month OR on any Friday."
This happens because the original Vixie cron implementation treats restricted day-of-month and day-of-week as independent triggers. If either condition is met, the job runs.
How to get AND behavior:
If you truly need "the 1st of the month, but only if it's a Friday," you have two options:
- Use one field and handle the other in your script:
0 0 1 * *
Then in your script:
#!/bin/bash
# Only proceed if today is Friday
if [ "$(date +%u)" -ne 5 ]; then
exit 0
fi
# ... actual job logic
- Use a cron implementation that supports AND logic. Some modern schedulers (like Quartz in Java) handle this differently. Check your platform's documentation.
The safe rule: If you need day-of-week filtering, set day-of-month to *. If you need day-of-month filtering, set day-of-week to *. Avoid restricting both simultaneously unless you understand the OR behavior.
Step Values, Ranges, and Lists in Depth
Step values (/) define the interval between matches:
*/10 * * * * → minutes 0, 10, 20, 30, 40, 50
0 */6 * * * → hours 0, 6, 12, 18
0 0 */2 * * → every 2nd day of the month (1st, 3rd, 5th...)
A common misconception: */5 in the minute field does NOT mean "every 5 minutes starting from when the job was created." It means "at minutes divisible by 5." If you create the job at 10:03, the next run is 10:05, not 10:08.
Ranges (-) define a continuous span:
0 9-17 * * * → every hour from 9 AM to 5 PM (inclusive)
0 0 * * 1-5 → midnight, Monday through Friday
0 0 1-15 * * → first 15 days of each month
Lists (,) combine discrete values:
0 8,12,18 * * * → 8 AM, noon, and 6 PM
0 0 1,15 * * → 1st and 15th of each month
You can combine all three:
0,30 9-17/2 * * 1-5
This means: at minutes 0 and 30, for hours 9, 11, 13, 15, 17, Monday through Friday.
Real-World Examples: CI/CD and Backups
CI/CD: Run tests every 15 minutes during development hours:
*/15 8-20 * * 1-5
Fires every 15 minutes from 8 AM to 8 PM, weekdays. Your CI pipeline picks up new commits and runs the test suite without burning compute overnight.
Database backup: Daily at 2 AM:
0 2 * * *
Simple and reliable. Pair it with a retention script:
#!/bin/bash
pg_dump mydb | gzip > /backups/mydb_$(date +%Y%m%d).sql.gz
find /backups -name "*.sql.gz" -mtime +30 -delete
Weekly backup: Sunday at 3 AM:
0 3 * * 0
Monthly report generation: 1st of month at 6 AM:
0 6 1 * *
Certificate renewal check: Every day at 4 AM (for Let's Encrypt 90-day certs):
0 4 * * *
With a script that only renews if expiry is within 30 days:
#!/bin/bash
certbot renew --quiet --deploy-hook "systemctl reload nginx"
Log rotation: Every Sunday at 1 AM:
0 1 * * 0
Health check ping: Every minute (use sparingly):
* * * * *
Running every minute is fine for lightweight health checks, but avoid expensive operations at this frequency. If your job takes longer than 60 seconds, add a lock file to prevent overlapping runs:
#!/bin/bash
flock -n /tmp/myjob.lock -c 'actual_command_here'
Validating Your Cron Expressions
Cron syntax errors are silent—there's no compiler to catch them. A typo like 0 9 * * 8 (invalid day-of-week) might be rejected by some cron daemons or silently ignored by others.
Before deploying a schedule to production, use the cron parser for a three-step check:
- Field expansion — confirm each field lands on the digits you meant (no accidental
*/50). - Readable summary — English phrase assembly (Month / Day / Weekday / Hour / Minute) for a quick scan of the value sets.
- Next 5 runs — wall-clock previews in the browser local timezone, with Vixie day/weekday OR semantics. For
0 0 1 * 5you will see both “1st of month at midnight” and “every Friday at midnight” — if you wanted AND, it fails here on purpose.
Timezone caveat: “next runs” use the clock of the machine viewing the page, not the target server’s TZ. Cloud VMs are often UTC; containers without TZ usually are too. Convert the preview to the server timezone, or drop a low-frequency probe that appends date to a log on the box, then keep the schedule once both sides agree.
If you're building from a requirement (“every 4 hours on weekdays”), the cron generator can point-and-click the expression and show the same next-run preview — then paste back into the parser for a second look.
Both tools run entirely in the browser. No expression is uploaded, which matters when schedules encode backup windows, deploy cadence, or maintenance slots.
Beyond five fields: macros, seconds, and dialects
Standard crontab also has macros (@hourly, @daily, @weekly, @monthly, @yearly, @reboot) with fixed meaning — they never go through five-field parsing. Kubernetes CronJob and most SaaS schedulers accept only five fields, not macros; pasting @daily into a CronJob YAML fails outright.
The reverse trap is one field too many: Quartz / Spring @Scheduled default to a seconds field (six or seven fields). Paste Linux 0 2 * * * into Quartz and the whole string shifts right — “never fires” or “fires every second.” Count fields before migrating: this site’s parser and generator bind classic five-field cron; six fields return INVALID_CRON_FORMAT.
Quick Reference Card
| Schedule | Expression |
|---|---|
| Every minute | * * * * * |
| Every 5 minutes | */5 * * * * |
| Every hour | 0 * * * * |
| Every day at midnight | 0 0 * * * |
| Weekdays at 9 AM | 0 9 * * 1-5 |
| Every Sunday at 2 AM | 0 2 * * 0 |
| 1st of month at noon | 0 12 1 * * |
| Every 6 hours | 0 */6 * * * |
| Twice daily (8 AM, 8 PM) | 0 8,20 * * * |
| Every 15 min, 9-5, weekdays | */15 9-17 * * 1-5 |
Bookmark this page, or better yet, keep a cron parser tab open whenever you're writing schedules. The five minutes you spend verifying an expression now saves the 3 AM page when a backup doesn't run.
Triage notes
The whole article is built around one trap: when both day-of-month and day-of-week are restricted, cron ORs them instead of ANDing — the intro warns that nearly everyone has been bitten. The safety rule is deliberately blunt: if you need weekday filtering, set the day field to *, and don't restrict both fields unless you clearly understand the OR behavior. The verification section exists because cron syntax errors are silent — no compiler catches them for you — and the "next 5 runs" step intentionally computes under OR semantics: 0 0 1 * 5 lists both the 1st of each month and every Friday, so an AND intent exposes itself on the spot. The dialect section's six pitfalls are equally typical: pasting a five-field expression into Quartz shifts the whole string, showing up as "never fires" or "fires every second".
Related Tools
Related Articles
Markdown to Word: Native Style Mapping and the Inline-Format Downgrade List
Turning a Markdown draft into a deliverable .docx: pandoc CLI vs in-browser local conversion, how headings/tables/code blocks map to native Word styles, which inline formats degrade to plain text, and why 'editable' beats 'looks right'.
Word to Markdown: Two Clean Paths and What Survives the Conversion
How to convert .docx to Markdown cleanly: pandoc CLI vs in-browser local conversion, a format fidelity table for headings/tables/images/auto-numbering, and fixes for the .doc legacy format, image bloat, and merged-cell tables.
China License Plate Rules and Validation: Standard, New Energy, and Special Formats Explained
Mainland China plate encoding rules in full: the standard blue plate, the GA36D-2018 new-energy format, armed police and embassy plates, province abbreviations and issuing-authority letters — with an online validator.