Fix Common Cron Expression Mistakes

2026-03-27 · SPUNK13 · spunk.bet

A cron expression that is wrong rarely errors. It just runs at some other time, or never, and you find out days later when a report is missing. Half of these failures are the expression itself and half are the environment the job runs in, which is far more stripped down than the shell you tested in. The sections below go through the specific traps in the order they usually bite, starting with the one that explains most pasted expressions behaving strangely. Every fix here can be verified in under a minute without waiting for the next scheduled run.

Count the fields before you debug anything else

Most "my expression is wrong" reports are a schedule written for one dialect and pasted into another. Standard Unix crontab takes five fields: minute, hour, day-of-month, month, day-of-week. Quartz and Spring's @Scheduled(cron=…) put seconds first, so they take six, and Quartz allows an optional seventh for year. AWS EventBridge also takes six, but its sixth is year, not seconds, and it requires ? in either day-of-month or day-of-week because it refuses to let you restrict both.

So 0 3 * * * is 3am daily in crontab and a parse error in Spring, where the same schedule is 0 0 3 * * *. If you paste a five-field expression into Spring it may still parse and run every minute at second zero of the hour you meant to be a minute. Check the field count first, every time.

Day-of-month and day-of-week combine with OR, not AND

This is in the POSIX spec and it surprises nearly everyone. When both day fields are restricted — neither is * — cron fires if either matches. 0 9 13 * 5 does not mean "9am on Friday the 13th". It means 9am on the 13th of every month and 9am every Friday. If you genuinely need the intersection, schedule the looser one and test the other condition in the script:

0 9 13 * *  [ "$(date +\%u)" = "5" ] && /opt/app/bin/report.sh

Step values count from the field's start, not from now

*/5 in the minute field is every five minutes, which is what people expect. 0 */5 * * * is every five hours — 00:00, 05:00, 10:00, 15:00, 20:00 — and then a four-hour gap across midnight, because the step restarts at hour 0 each day. Any step that does not divide evenly into the field's range produces that uneven wrap. To run on a schedule that respects the day boundary, list the hours: 0 0,6,12,18 * * *. Ranged steps like 0 9-17/2 * * 1-5 work and are clearer than the alternative.

Daylight saving eats runs and duplicates them

Vixie cron runs on local time. On a spring-forward day the local clock jumps from 01:59 to 03:00, so a job at 02:30 has no wall-clock moment to run in and is skipped. On the autumn transition 01:00–02:00 happens twice and a job in that window can run twice. Some cron implementations compensate for jobs scheduled inside the skipped hour and none of them do it identically, which is worse than a clear rule.

Two reliable fixes. Set the job's timezone to UTC — either the whole host, or a CRON_TZ=UTC line above the entry in cron implementations that honour it — and accept that a "9am" report drifts by an hour twice a year. Or schedule outside the transition window entirely: nothing between 01:00 and 03:00 local. Also make the job idempotent, because a double run should be boring rather than a duplicate invoice.

It works in your shell and does nothing in cron

cron gives the job a nearly empty environment. There is no .bashrc, no nvm, no pyenv shim, and PATH is often just /usr/bin:/bin. Use absolute paths for every binary and every file the script touches. The other silent killers:

Overlapping runs and how to verify before waiting

A five-minute job that occasionally takes seven minutes will eventually have two copies fighting over the same database rows. Wrap it: flock -n /var/lock/myjob.lock /opt/app/run.sh exits immediately if a run is already holding the lock. To confirm a schedule without waiting an hour, run the expression through croniter in Python and print the next ten fire times against a fixed start date — that catches the OR rule and the step-wrap immediately. For systemd timers, systemd-analyze calendar "Mon *-*-* 09:00:00" prints the normalised form and the next elapse. Finish with crontab -l to confirm the file the daemon actually loaded matches what you edited.

Explore More

Free tools, guides, and resources.

Visit spunk.bet
400+ ToolsCasinoMemesAstrologyScam DB