I once wrote a cron expression that was supposed to run a database backup every Sunday at 3 AM. What it actually did was run every minute of every hour on every Sunday. The backup script ran 1,440 times every Sunday for three weeks before someone noticed the disk was filling up with redundant backup files. The expression was * * * * 0 when it should have been 0 3 * * 0. One wrong field, and a weekly task becomes a one-per-minute task.
Cron syntax is one of those things that most developers use regularly but few have actually memorized. We look it up, copy an expression from Stack Overflow, tweak it, and hope it's right. That approach works until it doesn't, and when cron goes wrong, it tends to go wrong quietly.
The five fields
A standard cron expression has five fields, left to right:
minute hour day-of-month month day-of-week
0 3 * * 0
Each field accepts:
- A specific value:
5means the fifth minute, or the fifth month (May) - A wildcard:
*means every possible value - A range:
1-5means one through five - A step:
*/15means every 15th value - A list:
1,3,5means those specific values
The ranges for each field are:
- Minute: 0-59
- Hour: 0-23
- Day of month: 1-31
- Month: 1-12
- Day of week: 0-7 (both 0 and 7 represent Sunday)
Expressions people get wrong
"Every 5 minutes" is not 5 * * * *.
That runs at the fifth minute of every hour. Once per hour. The correct expression is */5 * * * *, which means "every minute where the minute value is divisible by 5."
"Every weekday at 9 AM" is 0 9 * * 1-5.
Not 0 9 * * 0-4. Monday is 1, Friday is 5. Sunday is 0 (or 7). I've seen this mistake in production more than once.
"The first Monday of every month" is hard.
Cron doesn't natively support "the first Monday." You can approximate it with 0 9 1-7 * 1, which means "9 AM on any Monday that falls within the first seven days of the month." But be aware: if day-of-month and day-of-week are both specified (not wildcards), standard cron treats them as OR, not AND. This means the job runs on any Monday OR any day from the 1st through the 7th.
The behavior of the OR logic between day-of-month and day-of-week is one of the most misunderstood aspects of cron. It's counterintuitive and varies between cron implementations.
The OR vs AND problem
In standard Vixie cron (the default on most Linux systems), when both day-of-month and day-of-week are non-wildcard, the job runs if either matches. So 0 9 15 * 1 runs at 9 AM on the 15th of every month AND on every Monday.
Some newer systems, like systemd timers and some cloud schedulers, use AND logic instead. AWS CloudWatch cron expressions use a ? wildcard to explicitly mark one of the two day fields as "don't care" to avoid this ambiguity.
# AWS CloudWatch: 9 AM on Mondays only
0 9 ? * MON *
Always check which cron implementation you're targeting. The syntax looks the same but the semantics differ.
Common scheduling patterns
Here's a reference table I keep handy:
# Every minute
* * * * *
# Every hour on the hour
0 * * * *
# Every day at midnight
0 0 * * *
# Every Sunday at 3 AM
0 3 * * 0
# Weekdays at 9 AM
0 9 * * 1-5
# Every 15 minutes during business hours
*/15 9-17 * * 1-5
# First day of every month at noon
0 12 1 * *
# Every quarter (Jan, Apr, Jul, Oct) on the 1st at 6 AM
0 6 1 1,4,7,10 *
# Twice a day at 8 AM and 8 PM
0 8,20 * * *
Timezone pitfalls
Cron jobs run in the timezone of the server, which is often UTC. If your server is UTC and you schedule a job for 0 9 * * * thinking it's 9 AM local time, it's actually 9 AM UTC, which might be 4 AM Eastern or 1 AM Pacific.
With cloud-based cron services (AWS EventBridge, Google Cloud Scheduler, Azure Logic Apps), you can usually specify the timezone explicitly. With traditional cron on a Linux server, you either set the system timezone or adjust the hour manually.
Daylight saving time adds another layer. If your server timezone observes DST, a job scheduled at 2:30 AM might run twice on the fall-back day or not at all on the spring-forward day. This is one of several reasons infrastructure teams prefer running servers in UTC.
Six-field cron and nonstandard extensions
Some systems use a six-field cron expression that adds a seconds field at the beginning:
# AWS and Spring: second minute hour day-of-month month day-of-week
0 */5 * * * * # Every 5 minutes (at second 0)
Others add a year field at the end. Quartz Scheduler (common in Java) uses seven fields. Jenkins uses its own variation. GitHub Actions uses standard five-field cron but wraps it in YAML with its own scheduling syntax.
The point is that "cron expression" isn't a single standard. It's a family of similar-but-not-identical syntaxes. Always read the docs for the specific system you're configuring.
Debugging cron jobs
When a cron job doesn't run, the problem is almost never the cron daemon itself. It's usually one of these:
Environment variables aren't set. Cron runs with a minimal environment. Your
PATH, database URLs, API keys -- none of that is inherited from your shell profile. Either set them in the crontab or source them in your script.Relative paths fail. A script that works when you run it manually might fail in cron because the working directory is different. Use absolute paths everywhere.
Output goes nowhere. By default, cron emails output to the user. If email isn't configured (and it usually isn't on modern systems), the output vanishes. Redirect stdout and stderr to a log file.
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
- Permissions. The cron job runs as the user whose crontab it's in. If that user doesn't have permission to read, write, or execute the necessary files, it fails silently.
For quick conversion between human-readable schedules and cron expressions, I built a cron expression generator at zovo.one/free-tools/cron-expression-generator that shows you the next execution times so you can verify the expression does what you think it does.
Cron is simple in concept but subtle in practice. The five-field syntax packs a lot of meaning into very little space, and one wrong value can be the difference between a daily job and a per-minute job. When in doubt, generate the expression, verify the next run times, and test it in a non-production environment first.
I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.
Top comments (0)