Cron expressions are one of those things you reach for repeatedly but never quite commit to memory. This page has two parts: real-world use cases (specific tasks with the cron expression that fits) and a complete reference organized by schedule frequency. If you know what you need to schedule but can’t remember the syntax, start with the use cases.
The right schedule depends on what the job does. Here are common real-world tasks with the cron expression that fits each one.
| Task | Expression | Notes |
|---|---|---|
| Nightly database backup | 0 2 * * * | 2am daily — low traffic, before business hours |
| Weekly full backup (off-peak) | 0 1 * * 0 | Sunday 1am; run shorter incrementals Mon–Sat |
| Log rotation / cleanup | 0 0 * * * | Midnight daily; clears yesterday’s logs |
| Temp file purge | 30 3 * * * | 3:30am — after backup finishes, before day starts |
| SSL certificate renewal check | 0 0 * * 1 | Weekly Monday; certbot renews ~30 days before expiry |
| Disk usage alert | 0 */6 * * * | Every 6 hours; alerts if disk > 85% |
| Security scan | 0 3 * * 0 | Sunday 3am weekly; avoid running during business hours |
| Database VACUUM / ANALYZE | 0 4 * * 0 | Sunday 4am; Postgres autovacuum may handle this already |
| Task | Expression | Notes |
|---|---|---|
| Daily email digest | 0 8 * * * | 8am UTC; users receive digest at the start of their day |
| Weekly summary report | 0 9 * * 1 | Monday 9am; lands in inboxes at start of work week |
| Monthly invoice generation | 0 6 1 * * | 1st of month at 6am; before customers check email |
| Subscription renewal reminders | 0 10 * * * | Daily at 10am; script filters which users expire in 7 days |
| Failed payment retry | 0 */4 * * * | Every 4 hours; retry with exponential backoff in code |
| Abandoned cart email | 30 1 * * * | 1:30am daily; catch carts abandoned >24h ago |
| Review request (e-commerce) | 0 10 * * 1-5 | Weekday mornings; higher open rates than weekends |
| Task | Expression | Notes |
|---|---|---|
| ETL from external API | 0 */2 * * * | Every 2 hours; respect API rate limits and TTL |
| CRM / database sync | */30 * * * * | Every 30 minutes during the day; full sync nightly |
| Search index rebuild | 0 3 * * * | 3am nightly; avoid during peak search traffic |
| Analytics aggregation | 5 0 * * * | 12:05am; after midnight boundary, aggregates yesterday’s data |
| Cache warmup (CDN/Redis) | 50 5 * * * | 5:50am; warms before 6am traffic spike begins |
| Sitemap regeneration | 0 1 * * * | 1am nightly; submit fresh sitemap to search engines |
| Exchange rate / price update | 0 */1 * * * | Hourly; or */15 9-17 * * 1-5 during market hours only |
| Quarterly data export | 0 6 1 */3 * | First day of each quarter (Jan, Apr, Jul, Oct) at 6am |
| Task | Expression | Notes |
|---|---|---|
| Nightly build / test suite | 0 2 * * * | 2am; run full suite overnight, report by 6am |
| Dependency update check | 0 9 * * 1 | Monday 9am; opens PRs for outdated packages weekly |
| Staging environment reset | 0 0 * * * | Midnight; resets staging to prod snapshot for clean tests |
| Automated smoke test | */15 * * * * | Every 15 minutes; quick end-to-end health check |
| Docker image cleanup | 0 4 * * 0 | Sunday 4am; prunes dangling images / old containers |
| Kubernetes node health check | */5 * * * * | Every 5 minutes; escalate if node not ready >10 min |
| Task | Expression | Notes |
|---|---|---|
| Uptime check | */5 * * * * | Every 5 minutes; dedicated tools (Uptime Robot) are better |
| Dead link checker | 0 0 * * 0 | Weekly Sunday midnight; broken link scan across the site |
| Error rate alert | */10 * * * * | Every 10 minutes; check Sentry/Datadog error counts |
| Daily metrics email | 0 7 * * 1-5 | Weekday 7am; key business metrics before standup |
| API budget check | 0 0 * * * | Daily midnight; alert if approaching API quota limits |
| Expression | Meaning |
|---|---|
* * * * * | Every minute |
*/2 * * * * | Every 2 minutes |
*/5 * * * * | Every 5 minutes |
*/10 * * * * | Every 10 minutes |
*/15 * * * * | Every 15 minutes |
*/30 * * * * | Every 30 minutes |
The */n syntax means "every n units." */15 * * * * fires at :00, :15, :30, and :45 of each hour. Note that */7 * * * * does not fire "every 7 minutes consistently" — it fires at minutes 0, 7, 14, 21, 28, 35, 42, 49, 56, and then resets at the top of the next hour. If consistent intervals across hours matter, use a different approach.
| Expression | Meaning |
|---|---|
0 * * * * | Every hour (at :00) |
0 */2 * * * | Every 2 hours |
0 */4 * * * | Every 4 hours (midnight, 4am, 8am, noon, 4pm, 8pm) |
0 */6 * * * | Every 6 hours (midnight, 6am, noon, 6pm) |
0 */12 * * * | Every 12 hours (midnight and noon) |
30 */4 * * * | Every 4 hours, at :30 past the hour |
| Expression | Meaning |
|---|---|
0 0 * * * | Every day at midnight (12:00 AM) |
0 1 * * * | Every day at 1:00 AM |
0 3 * * * | Every day at 3:00 AM |
0 6 * * * | Every day at 6:00 AM |
0 9 * * * | Every day at 9:00 AM |
0 12 * * * | Every day at noon (12:00 PM) |
0 17 * * * | Every day at 5:00 PM |
0 23 * * * | Every day at 11:00 PM |
30 8 * * * | Every day at 8:30 AM |
15 14 * * * | Every day at 2:15 PM |
The @daily shorthand is equivalent to 0 0 * * * on most systems.
| Expression | Meaning |
|---|---|
0 9 * * 1-5 | Every weekday (Mon–Fri) at 9:00 AM |
0 8 * * 1-5 | Every weekday at 8:00 AM |
0 18 * * 1-5 | Every weekday at 6:00 PM |
*/30 9-17 * * 1-5 | Every 30 minutes during business hours (9am–5pm), weekdays |
0 0 * * 6,0 | Every weekend (Saturday and Sunday) at midnight |
0 10 * * 1 | Every Monday at 10:00 AM |
0 10 * * 5 | Every Friday at 10:00 AM |
In standard cron, 1-5 means Monday through Friday. Day-of-week values: 0 or 7 = Sunday, 1 = Monday, …, 6 = Saturday.
*), cron uses OR logic — the job runs if either condition matches. 0 12 15 * 5 fires on the 15th of every month and on every Friday. To get AND semantics (only on Fridays that fall on the 15th), you need a script-level check: run [ $(date +%d) -eq 15 ] && your-command.
| Expression | Meaning |
|---|---|
0 0 * * 0 | Every Sunday at midnight |
0 9 * * 1 | Every Monday at 9:00 AM |
0 10 * * 3 | Every Wednesday at 10:00 AM |
0 8 * * 6 | Every Saturday at 8:00 AM |
0 2 * * 0 | Every Sunday at 2:00 AM (weekly maintenance window) |
The @weekly shorthand is equivalent to 0 0 * * 0.
| Expression | Meaning |
|---|---|
0 0 1 * * | First day of every month at midnight |
0 9 1 * * | First day of every month at 9:00 AM |
0 0 15 * * | 15th of every month at midnight |
0 0 L * * | Last day of every month (Quartz/Spring syntax — not standard cron) |
0 0 1 */3 * | First day of every quarter (Jan, Apr, Jul, Oct) at midnight |
0 0 1 1 * | January 1st at midnight (yearly) |
0 0 28-31 * * does not reliably mean "last day of the month." In February, day 28 exists every year, but days 29–31 don't. The job will still fire on the 28th — and on days 29, 30, 31 in months that have them. For last-day-of-month logic, use a wrapper script: [ $(date +%d) -eq $(cal | awk 'NF{f=$NF} END{print f}') ] && your-command.
| Expression | Meaning |
|---|---|
0,30 * * * * | Every hour at :00 and :30 |
0 6,12,18 * * * | Three times daily: 6am, noon, 6pm |
0 0 * * 1,3,5 | Monday, Wednesday, Friday at midnight |
0 9-17 * * 1-5 | Every hour from 9am to 5pm, weekdays |
5 4 * * 0 | Every Sunday at 4:05 AM |
23 */2 * * * | At minute 23 of every even-numbered hour |
0 4 1,15 * * | 1st and 15th of every month at 4:00 AM |
Commas in any field mean "or" — 1,3,5 in the day-of-week field means Monday, Wednesday, or Friday.
Most cron implementations support convenient @-shorthands:
| Shorthand | Equivalent | Meaning |
|---|---|---|
@yearly | 0 0 1 1 * | Once a year, January 1st at midnight |
@annually | 0 0 1 1 * | Same as @yearly |
@monthly | 0 0 1 * * | Once a month, first day at midnight |
@weekly | 0 0 * * 0 | Once a week, Sunday at midnight |
@daily | 0 0 * * * | Once a day at midnight |
@midnight | 0 0 * * * | Same as @daily |
@hourly | 0 * * * * | Once an hour at :00 |
@reboot | N/A | Once, at system startup (not supported everywhere) |
GitHub Actions uses standard 5-field cron syntax in UTC. The schedule trigger fires approximately on time but not exactly — GitHub queues jobs during high-load periods.
on:
schedule:
- cron: '0 6 * * 1-5' # Weekdays at 6:00 AM UTC
- cron: '*/15 * * * *' # Every 15 minutes (note: not recommended for high-frequency polling)
- cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC
Important: GitHub Actions cron jobs that haven't run for 60 days on an inactive repository will be disabled automatically.
AWS supports two formats: cron (6 fields, with year) and rate expressions.
# AWS cron format: cron(minute hour day-of-month month day-of-week year)
cron(0 9 * * ? *) # Every day at 9:00 AM UTC (? = no specific day-of-week)
cron(0 */4 * * ? *) # Every 4 hours
cron(0 0 1 * ? *) # First day of every month at midnight
# Rate expressions (simpler for intervals)
rate(5 minutes)
rate(1 hour)
rate(1 day)
AWS uses ? (not *) when day-of-month and day-of-week conflict. You must use ? in one of the two fields when the other is specified.
Kubernetes uses standard 5-field cron syntax in the server's local timezone (usually UTC).
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
spec:
schedule: "0 3 * * *" # Every day at 3:00 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: report
image: myapp:latest
command: ["python", "report.py"]
The popular node-cron library uses a 5 or 6-field format (optionally with seconds as the first field):
import cron from 'node-cron';
// Standard 5-field
cron.schedule('0 * * * *', () => console.log('Every hour'));
cron.schedule('*/5 * * * *', () => console.log('Every 5 minutes'));
// 6-field (with seconds as first field)
cron.schedule('*/30 * * * * *', () => console.log('Every 30 seconds'));
0 */4 * * * (every 4 hours) is not the same as */4 0 * * * (every 4 minutes during the midnight hour only). The order matters: minute, hour, day, month, weekday.
Standard cron runs in the server's local timezone. If your server is UTC and you want a job to run at 9am New York time (EST/EDT), you need to account for the offset: 0 14 * * * in UTC for EST (+5), 0 13 * * * for EDT (+4). Daylight saving transitions can cause jobs to shift by an hour.
0 * * * * fires once an hour, not once a minute. * * * * * fires every minute. A single 0 in the minute field means "at minute 0 (the top of the hour)."
If your job takes 45 minutes and runs every 30 minutes, you'll have overlapping runs. Most cron implementations don't enforce single-instance behavior — add a lock file or use flock:
*/30 * * * * flock -n /tmp/myjob.lock /opt/jobs/myjob.sh
Paste any expression into the free cron parser to see a plain-English description, field-level validation, and the next 5 run times in your local timezone. Works offline, no signup.
Open cron expression parser →Getting the expression right is half the job. The harder part — and the one that actually causes incidents — is knowing when a scheduled job quietly stops running. Cron doesn't alert you when a job misses its window. It just doesn't run.
The pattern that works: a dead man's switch. Your cron job sends an HTTP ping at the end of each successful run. If the monitoring service doesn't receive the ping within the expected window, it alerts you. One line at the end of your job catches three failure modes at once: the job didn't start, the job failed, or the job was killed before completing.
CronWatch adds a dead man's switch to your existing cron jobs. Add one curl line to your script. If the job misses its window, you get an alert. Works with crontab, GitHub Actions, Heroku Scheduler, AWS EventBridge, and Kubernetes CronJob.
Join the CronWatch waitlist → Parse an expression