Master cron expressions with this in-depth guide. Learn cron syntax, common scheduling patterns, timezone handling, special strings, testing strategies, and platform differences for Linux, AWS, Kubernetes, and more.
Cron expressions used to terrify me
Those five asterisks looked like some ancient incantation that could either run my backup script perfectly or delete everything at 3 AM. After one particularly memorable incident at Šikulovi s.r.o. where a misconfigured cron job sent 50,000 emails in an hour, I decided to actually learn how these things work. Spoiler: they're not that complicated once you get the pattern down.
The word 'cron' comes from the Greek 'chronos' meaning time, which is about as poetic as Unix gets. The cron daemon sits there patiently reading your crontab files and firing off commands when the time comes. It's been doing this since the 1970s and honestly, the syntax hasn't changed much. That's either reassuring or terrifying depending on how you look at it.
Five fields, left to right, every time
Here's the mental model that finally made cron click for me: read it left to right like a sentence about time, starting with the smallest unit. Minute, hour, day of month, month, day of week. That's it. 30 9 * * 1 literally reads as 'minute 30, hour 9, any day, any month, Monday.'
- Minute (0-59): Which minute of the hour
- Hour (0-23): 24-hour format, so 13 = 1 PM
- Day of month (1-31): The calendar date
- Month (1-12): Or use JAN, FEB, etc.
- Day of week (0-6): Sunday is 0, Saturday is 6. Both 0 and 7 work for Sunday (compatibility quirk)
- Example: 30 9 * * 1 = Monday at 9:30 AM
The special characters that actually matter
You only need to master four characters for 90% of scheduling tasks. The asterisk means 'any', the comma separates multiple values, the hyphen creates ranges, and the slash creates steps. Everything else is platform-specific extras.
- * (asterisk): Any value - the wildcard
- , (comma): Multiple values - 1,15,30 means 1st, 15th, and 30th
- - (hyphen): Ranges - 1-5 means Monday through Friday
- / (slash): Steps - */5 means every 5, */10 means every 10
- L, W, #, ?: These exist but are platform-specific - check your docs
The expressions I use every week
I probably write the same 10 cron expressions over and over. Here are the ones that cover most real-world scenarios. I used to have these on a sticky note on my monitor before I built the Cron Generator tool.
- */5 * * * * = Every 5 minutes (health checks, polling)
- 0 * * * * = Top of every hour (hourly reports)
- 0 0 * * * = Midnight daily (cleanup jobs)
- 0 6 * * * = 6 AM daily (morning reports before standup)
- 0 9-17 * * 1-5 = Hourly during business hours, weekdays only
- 0 0 * * 0 = Sunday midnight (weekly maintenance)
- 0 0 1 * * = First of the month (billing, monthly reports)
- 0 0 1 1 * = January 1st (annual stuff)
The @shortcuts most people forget exist
I went years without knowing about these. Most cron implementations support readable shortcuts that save you from counting asterisks. They're not universal though, so I always test before relying on them in production.
- @yearly or @annually = 0 0 1 1 * (once a year)
- @monthly = 0 0 1 * * (first of the month)
- @weekly = 0 0 * * 0 (Sunday midnight)
- @daily or @midnight = 0 0 * * * (daily at midnight)
- @hourly = 0 * * * * (top of each hour)
- @reboot = Run once when the system starts (not time-based)
Timezones will ruin your day
I've been burned by timezones so many times that I now put a comment above every single cron entry stating what timezone I think it's running in. DST transitions are particularly nasty - your 2:30 AM job might not exist or might run twice depending on the direction of the change.
- Linux cron uses whatever is in /etc/timezone
- AWS EventBridge and GitHub Actions are always UTC (no exceptions)
- Kubernetes uses the controller timezone unless you specify otherwise
- DST transitions: Jobs at 2:30 AM are playing with fire
- My rule: UTC for anything distributed, local only when I must
- Always comment: # Runs in UTC or # Runs in server local time (Europe/Prague)
Test before you deploy (I mean it)
Every cron mistake I've made could have been caught by testing. I once deployed a job that was supposed to run daily but ran every minute because I had the fields in the wrong order. 1,440 executions before I noticed. Now I always preview the next few run times in the Cron Generator before pushing anything to production.
- Preview at least 5-10 upcoming runs
- Check the human-readable description matches your intent
- Test month boundaries, leap years, DST transitions
- For new jobs, I sometimes start with */5 * * * * to verify it works, then switch to the real schedule
- Use the Cron Generator to build and validate visually
Linux crontab: the original
If you're on a Linux server, crontab is probably what you're using. It's the OG, the one everything else is based on. A few things caught me off guard when I started: the environment is basically empty, output goes to email by default, and there's no built-in logging.
- crontab -e to edit, crontab -l to list, crontab -r to remove (careful with that last one)
- Use full paths everywhere: /usr/bin/python not python
- MAILTO="" at the top to stop email spam
- Redirect output: >> /var/log/job.log 2>&1
- User crontabs: /var/spool/cron/crontabs/
- System crontab: /etc/crontab (includes user field)
AWS EventBridge: the quirky cousin
AWS decided cron needed a sixth field (year) and a question mark placeholder. It's just different enough to break your muscle memory. I've wasted hours debugging why my expression works locally but not in AWS.
- 6 fields: minute hour day-of-month month day-of-week year
- ? is required for either day-of-month OR day-of-week (can't use * for both)
- Always UTC, no timezone configuration
- Example: cron(0 9 ? * MON *) = Every Monday at 9 AM UTC
- L works for last day: cron(0 0 L * ? *)
- Rate expressions as alternative: rate(5 minutes)
Kubernetes CronJobs: containerized scheduling
Kubernetes uses standard 5-field cron but adds its own complexity. concurrencyPolicy is the setting I always forget to configure - by default it allows overlapping runs, which can be disastrous for long-running jobs.
- Standard 5-field: schedule: "0 */6 * * *"
- Timezone: defaults to controller, use timeZone field for explicit setting (v1.25+)
- concurrencyPolicy: Forbid prevents overlapping (I almost always want this)
- startingDeadlineSeconds: how long to still run if missed
- suspend: true to pause without deleting
GitHub Actions: the 5-minute minimum
GitHub throttles anything more frequent than every 5 minutes, and scheduled workflows can be significantly delayed during busy periods. I've seen 10+ minute delays on free tier. Also, they disable your scheduled workflows after 60 days of repo inactivity. Found that out the hard way.
- Format: schedule: - cron: "0 5 * * *"
- Always UTC, no exceptions
- 5-minute minimum interval
- Workflows may be delayed during high load
- Disabled after 60 days of repo inactivity
- Add workflow_dispatch for manual runs alongside schedule
When your job just... does not run
Silent failures are the worst. The job doesn't run, nothing is logged, you have no idea why. Here's my debugging checklist, roughly in order of likelihood:
- Is cron actually running? systemctl status cron
- Does the script have execute permissions? chmod +x
- Are you using absolute paths?
- Test the command manually as the cron user: sudo -u www-data /path/to/script.sh
- Check logs: /var/log/cron, /var/log/syslog, or journalctl -u cron
- Add logging to your script: echo "Started at $(date)" >> /var/log/job.log
- Timezone mismatch?
The day-of-month vs day-of-week trap
This one bit me hard. If you specify both day-of-month AND day-of-week (not asterisks), standard cron uses OR logic. So 0 9 15 * 1 means '9 AM on the 15th OR any Monday' not '9 AM on the 15th if it's a Monday.' Almost nobody wants OR logic. I always use * for one of them now.
- 0 9 15 * 1 = 9 AM on the 15th AND every Monday (probably not what you want)
- AWS uses ? to explicitly disable one field
- Want 'first Monday of month'? You need script logic or a different scheduler
- My rule: always use * for one of these fields
Mistakes I see constantly
After reviewing countless crontabs at Šikulovi s.r.o. and in clients' systems, these are the patterns that cause problems over and over:
- * * * * * for non-critical tasks: 1,440 runs per day, usually unnecessary
- Midnight pile-up: Everyone schedules at 0 0 * * *, server chokes
- Ignoring output: Fills up email or /var/spool
- No locking: Long job overlaps with itself
- No logging: Job fails silently for weeks
- No alerting: Nobody notices until customer reports it
How I set up production cron jobs now
After years of learning the hard way, here's my standard checklist for any production cron job. It takes 10 extra minutes upfront but saves hours of debugging later.
- Comment above every entry: # Daily backup - runs at 2 AM UTC - owner: [email protected]
- Stagger start times: 2:00, 2:15, 2:30, not all at 2:00
- Use flock for locking: flock -n /tmp/job.lock /path/to/script.sh
- Log everything: Start time, end time, exit code
- Alert on failure: Email, Slack, PagerDuty, something
- Version control the crontab
- Test in staging first
When cron is not enough
Cron is great for simple scheduling, but sometimes you need more. Job dependencies, retries, distributed execution, observability - if you need any of these, consider alternatives. I use cron for simple stuff and reach for other tools when complexity grows.
- systemd timers: Better logging, dependency management (Linux)
- Celery Beat: Python task scheduling (if you already use Celery)
- Apache Airflow: Complex workflows with dependencies
- Temporal: Durable execution with retries
- Kubernetes CronJobs: Container-native with resource limits
- My rule: Start with cron, migrate when you need dependencies or observability
My takeaway after 10+ years
Cron is deceptively simple. Five fields, a few special characters, and you can schedule almost anything. The hard part isn't the syntax - it's all the surrounding concerns: timezones, logging, locking, alerting, testing. Get those right and cron just works. Get them wrong and you'll be debugging at 3 AM like I did.
I built the Cron Generator tool because I was tired of making preventable mistakes. Use it to build your expressions, preview the next run times, and copy them with confidence. Your future self will thank you.
FAQ
What does * * * * * mean in cron?
The expression * * * * * means "every minute of every hour of every day of every month on every day of the week"—in other words, the job runs every single minute. Each asterisk means "any value" for that field. This is typically used only for testing or very high-frequency monitoring tasks.
How do I run a cron job every 5 minutes?
Use */5 * * * * to run every 5 minutes. The */5 in the minute field means "every 5th minute" (0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55). You can use */n in any field to create step intervals.
Why is my cron job not running?
Common causes include: the cron service not running, incorrect file permissions, missing execute permission on scripts, commands not using absolute paths, environment variables not available in cron environment, timezone differences, or syntax errors in the expression. Check cron logs and test commands manually.
What is the difference between 0 and 7 for Sunday in cron?
Both 0 and 7 represent Sunday in the day-of-week field. This exists for compatibility with different cron implementations. Most systems accept both values, but 0 is more commonly used. When using names (SUN, MON, etc.), there is no ambiguity.
How do I schedule a job for the last day of every month?
Standard cron does not support "last day of month" directly since months have different lengths. Some implementations support L (e.g., 0 0 L * *). Alternatively, use a script that checks if tomorrow is the 1st, or schedule for days 28-31 and have the script verify the date.
Are cron schedules in UTC or local time?
It depends on the platform. Linux cron uses the system timezone. AWS EventBridge and GitHub Actions always use UTC. Kubernetes CronJobs use the controller timezone by default but support explicit timezone configuration. Always check your platform documentation and document the expected timezone.
Can I use cron to run a job every 90 minutes?
Not directly with a single expression, since 90 does not divide evenly into 60 minutes or 24 hours. You would need multiple cron entries (e.g., jobs at 0:00, 1:30, 3:00, 4:30, etc.) or use a different scheduling approach like a daemon that sleeps between runs.
What happens if a cron job takes longer than the interval?
By default, cron will start another instance of the job at the next scheduled time, even if the previous run is still executing. This can cause resource exhaustion and data corruption. Use file-based locking (flock), PID files, or your scheduler's concurrency controls to prevent overlapping runs.