Crontab Validation: Common Mistakes and How to Fix Them
Cron expressions have caused me more 3 AM incidents than I care to admit. Here is my guide to validating them properly, plus every mistake I have made so you do not have to.
Ready-to-use cron expressions for every common task: every minute, hourly, daily at midnight, weekly, monthly, weekdays only, and more. Includes Unix crontab, AWS EventBridge, GitHub Actions, and Kubernetes CronJob formats.
I've been writing cron expressions for over a decade now, and I still catch myself double-checking the field order. Is it minute-hour-day or hour-minute-day? (It's minute first, always.) After one too many 3 AM pager alerts from misconfigured schedules at Šikulovi s.r.o., I started keeping a personal cheat sheet. This is that cheat sheet, battle-tested and ready to copy-paste.
Here's the thing about cron: the syntax is simple once you internalize it, but getting it wrong can be expensive. I once scheduled a cleanup job to run every minute instead of every hour because I put the asterisk in the wrong field. That's 60x the database load. Not fun. So bookmark this, and let's make sure your schedules actually do what you think they do.
Standard cron has 5 fields, read left to right: minute (0-59), hour (0-23), day of month (1-31), month (1-12), day of week (0-6 where Sunday is 0). I remember it as 'minutes matter most' - minute comes first. Special characters: * means any, */n means every n, n-m is a range, n,m is a list.
# ┌───────────── minute (0-59)
# │ ┌───────────── hour (0-23)
# │ │ ┌───────────── day of month (1-31)
# │ │ │ ┌───────────── month (1-12)
# │ │ │ │ ┌───────────── day of week (0-6, Sun=0)
# │ │ │ │ │
# * * * * *
0 9 * * * # 9:00 AM every day
*/15 * * * * # Every 15 minutes
0 0 1 * * # Midnight on the 1st of each monthRunning something every minute sounds harmless until you realize that's 1,440 executions per day. I use this for critical health checks only. If your script takes more than a minute to run, you'll have overlapping executions - been there, debugged that.
These are my go-to intervals for most polling tasks. */5 is great for cache refreshes, */15 for API syncs. Pro tip: if you're checking an external API, don't pick an exact interval like */10 - add some randomness or offset to avoid hammering their servers at the same time as everyone else.
Hourly is where I put log rotation, lightweight reports, and data syncs. The key decision is: do you want minute 0 (top of the hour) or some offset? I prefer offsets like :15 or :30 to spread load and avoid the 'thundering herd' problem where everyone's cron jobs fire at :00.
Most of my cron jobs run daily. Backups go at 2 AM, reports at 6 AM before the team wakes up. I avoid midnight exactly because every dev and their dog schedules things at 0 0 * * *. Spreading things out to 2 AM, 3 AM, 4 AM keeps the server happy.
When something needs to run a few times daily, I use comma-separated hours. The 9,17 pattern (9 AM and 5 PM) is classic for notifications. For business hours coverage, the range syntax 9-17 gives you every hour during work time.
Weekly reports, maintenance windows, end-of-week summaries - these all go here. I schedule heavy maintenance for Saturday or Sunday nights. Quick tip: day 0 is Sunday, day 1 is Monday. I always have to look this up. Always.
Most business logic only needs to run Monday through Friday. The 1-5 range in the weekday field is your friend here. I use this for all customer-facing notifications - nobody wants an automated email at 3 AM on Saturday.
Billing, invoices, monthly reports, archive jobs - these run on the 1st. Sometimes I'll also do a mid-month run on the 15th for certain metrics. If you need 'last day of month', that's tricky - standard cron doesn't support it directly. You'll need a script that checks the date.
Payroll, billing cycles, subscription renewals - these often have fixed dates. The L character for 'last day' is supported in some systems (Quartz, AWS) but not standard cron. For first-Monday-of-month type schedules, you need to get creative with 1-7 combined with weekday.
Quarterly financial reports run on the 1st of January, April, July, October. For annual jobs like license renewals or year-end processing, you set a specific month and day. These are rare but when you need them, here they are.
Here's what I actually run for database maintenance at Šikulovi s.r.o.. Stagger your jobs by 30-60 minutes so they don't compete for resources. I learned this the hard way when backup and vacuum ran simultaneously and the server ground to a halt.
I've seen servers die because nobody cleaned up logs. Set up rotation and cleanup early, before your disk fills up at 3 AM on a holiday weekend. Trust me on this one.
Health checks and monitoring need to balance frequency against overhead. Critical services get every-minute checks, everything else gets 5-minute intervals. Daily summary emails go out in the morning before standup.
Nightly builds, security scans, dependency updates - these are scheduled to run during off-hours so they don't slow down developer machines or consume CI resources during the day.
Different platforms have different cron formats. AWS EventBridge adds a 6th field (year) and uses ? as a placeholder. Kubernetes uses standard 5-field but watch your timezone - it defaults to the controller's timezone, not UTC. GitHub Actions is always UTC, no configuration possible.
# AWS EventBridge (6 fields with year)
cron(0 9 * * ? *)
# GitHub Actions
on:
schedule:
- cron: '0 9 * * 1-5'
# Kubernetes CronJob
spec:
schedule: "*/5 * * * *"I run everything in UTC now. Daylight saving time has bitten me too many times - jobs that skip or double-run because 2:30 AM doesn't exist or exists twice during DST transitions. Just use UTC and convert in your application code. Future you will thank present you.
I built the Cron Generator tool specifically because I was tired of deploying cron jobs and waiting to see if they ran correctly. Always preview the next few run times before pushing to production. Check edge cases like the 31st of the month or February 29th. A few minutes of testing saves hours of debugging why your job didn't run.
Use 0 0 * * * to run at midnight (00:00) every day. The first 0 is the minute, the second 0 is the hour, and the asterisks mean every day of month, every month, and every day of week.
Use */5 * * * * to run every 5 minutes. The */5 in the minute field means every 5th minute. You can use the same pattern for other intervals like */10, */15, or */30.
Use 1-5 in the day-of-week field (the 5th field). For example, 0 9 * * 1-5 runs at 9:00 AM Monday through Friday. Days are numbered 0-6 where 0 is Sunday.
Use 0 0 1 * * to run at midnight on the 1st of every month. Change the first two numbers to adjust the time, for example 0 9 1 * * for 9:00 AM on the 1st.
Standard cron does not support "last day" directly. Some systems support L in the day field (0 0 L * *). Alternatively, use a script that checks if tomorrow is the 1st before running.
Use comma-separated values. For example, 0 9,12,18 * * * runs at 9:00 AM, 12:00 PM, and 6:00 PM daily. You can combine this with ranges: 0 8-17 * * 1-5 runs every hour from 8 AM to 5 PM on weekdays.
Day of month (field 3) specifies dates 1-31. Day of week (field 5) specifies 0-6 (Sunday-Saturday). If both are set to non-asterisk values, the job runs when either condition matches, not both.
For long-running jobs, use a lock file or flock command to prevent overlapping executions. For example: flock -n /tmp/myjob.lock /path/to/script.sh will skip if the previous run is still active.
Founder of CodeUtil. Web developer building tools I actually use. When I'm not coding, I experiment with productivity techniques (with mixed success).
Cron expressions have caused me more 3 AM incidents than I care to admit. Here is my guide to validating them properly, plus every mistake I have made so you do not have to.
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.
I used to push broken code constantly. Now my git hooks catch linting errors, run tests, and validate commits before they embarrass me. Here's my exact setup.