Cron Scheduling Gotchas

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.