Cron Scheduling Gotchas
- cron
- scheduling
- operations
Cron has been quietly running the world's unglamorous jobs for forty years, and it will still be running them when today's orchestrators are blog posts about orchestration. That longevity breeds complacency: cron feels fully known. It is not. Here are the scheduling behaviors that have bitten me or people whose incident reports I have read, with the least painful migration path for each.
1. The DST spring-forward hour does not exist
Jobs scheduled in 02:00–02:59 in a DST-observing timezone simply do not run on the spring-forward night. Not delayed — skipped. Vixie cron matches wall-clock times; if the wall clock jumps from 02:00 to 03:00, no minute of 02:xx occurs, so no job fires.
If the job matters — backups, certificate renewal, billing cutoff —
either schedule it outside the DST jump window (03:30 or later, or
better 04:00), or run the daemon with CRON_TZ=UTC and
express the schedule in UTC, where every hour occurs exactly once.
2. The fall-back hour runs twice
The mirror problem: on the autumn night, 02:30 happens twice, and so does your job. Most of the time this is harmless. If the job is not idempotent — an increment, a charge, a notification — it is an incident that occurs once per year per job, always on a Sunday, at a time chosen by a 19th-century railway convention.
This is the one class of bug that crontab-doctor finds on nearly every server it is pointed at: non-idempotent jobs with a minute field inside 02:00–02:59 in a local timezone. The fix is the same as above — UTC, or idempotency, or both.
3. Day-of-month and day-of-week are OR'ed
The POSIX behavior that surprises everyone exactly once: if both
the day-of-month and day-of-week fields are restricted
(i.e., not *), the job runs when either
matches, not both. 0 0 13 * FRI runs on every Friday
and on the 13th — not on Friday the 13th. This is
documented in the man page, four words at a time, in the section
nobody re-reads.
4. % is special in the command
In a crontab, the first unescaped % in the command
field ends the command and starts standard input. So
date +%F silently becomes date + with
F piped in. Escape it: date +\%F. Every
old crontab has one of these waiting. My tool checks for it, which
is how I know.
5. The environment is 1975-sized
Cron runs your job with a minimal environment: typically
SHELL, PATH equal to
/usr/bin:/bin, and a cwd of your home directory. No
$HOME/.profile, no systemd
EnvironmentFile, none of your shell convenience. Jobs
that work interactively and fail from cron are usually failing on
one of three things: PATH, a missing variable, or a relative path.
The debugging ritual that always finds it:
# run the job in the same desert cron uses
env -i SHELL=/bin/sh PATH=/usr/bin:/bin \
HOME=$HOME /bin/sh -c 'your command here'
The quiet migration path
If the above has convinced you to move scheduling off local time, resist the rewrite. Cron files are the one thing every operator can read; do not replace them with a system only your team can read, in service of a problem you can fix with one line per file:
# at the top of every crontab, from now on
CRON_TZ=UTC
# and translate existing entries once:
# old: 30 2 * * * /usr/local/bin/backup.sh (Europe/Zurich)
# new: 30 0 * * * /usr/local/bin/backup.sh (UTC, same instant)
Verify the translation with a one-shot dry run on a test box by
faking the clock, or at minimum by comparing
date -d "02:30 tomorrow Europe/Zurich" -u against the
new minute field. Write the old wall-clock time in a comment —
future you reading the file during an incident thinks in wall
clock, not in UTC offsets, and the comment is the only translation
table that will be at hand.
When you have outgrown cron
You have not outgrown cron because it lacks features. You have
outgrown it when you need these, and only these: dependency
ordering between jobs, retries with backoff, or jobs that must not
overlap themselves. Until then, cron plus a locking wrapper
(flock is in your base system already) is the boring,
correct answer. The day you genuinely need the other three
features, choose a scheduler on its scheduling merits — not because
a conference talk told you cron is dead. Cron attended that talk.
Cron will outlive it.