The "works on my machine" of scheduling
You write a shell script, test it in your terminal, it runs fine. You drop it into a crontab, wait, and... nothing happens. Or it runs but fails silently, producing zero output and zero errors.
The gap between an interactive shell and a cron execution environment is wider than most developers expect. Here are the four failure modes that catch people most often.
1. The minimal PATH trap
The most common silent failure: the $PATH difference.
Cron launches scripts in a stripped-down shell (usually /bin/sh) that ignores your .bashrc, .zshrc, and everything else in your profile.
Your interactive terminal might have a $PATH with Node.js binaries, Python virtualenvs, and /usr/local/bin. Cron's default is usually just /usr/bin:/bin.
The Failure:
# This will likely fail with "command not found"
0 2 * * * python script.py >> output.log 2>&1
The Fix: Always use absolute file paths for commands, scripts, and output destinations.
# Safe configuration using absolute paths
0 2 * * * /usr/bin/python3 /home/user/app/script.py >> /home/user/app/output.log 2>&1
2. Execution pile-up (overlapping jobs)
Cron daemons are stateless. They fire a job when the clock matches, regardless of whether the previous run already finished.
If a job runs every 5 minutes (*/5 * * * *) but a slow database query pushes a single run to 12 minutes, the daemon will spawn a second process at minute 5 and a third at minute 10. The result: pile-ups, lock contention, and OOM kills.
The fix (Linux):
Use flock for an exclusive file lock.
*/5 * * * * /usr/bin/flock -n /tmp/myjob.lock /usr/local/bin/myjob.sh
flock -n grabs an exclusive lock before executing. If another process holds the lock, it exits immediately instead of spawning a duplicate.
The fix (Kubernetes):
Set concurrencyPolicy in your CronJob manifest.
spec:
concurrencyPolicy: Forbid
With Forbid, the controller skips a new run if the previous Pod hasn't finished.
3. Timezone offsets and DST
Most server infrastructure runs on UTC. If you schedule based on local business hours ("send a report at 2:00 AM"), Daylight Saving Time transitions will break your schedule.
- The Spring-Forward Skipped Hour: When clocks advance one hour in the spring, standard time often skips directly from 01:59 to 03:00. Any job configured to run inside this skipped hour (e.g.,
0 2 * * *) has no matching system time and will simply be skipped for that day. - The Fall-Back Repeated Hour: When clocks fall back, the interval from 01:00 to 01:59 happens twice. A standard cron job scheduled in this window will execute twice.
The fix:
Kubernetes (.spec.timeZone since v1.27) and AWS EventBridge Scheduler handle IANA timezone databases natively. On a bare Linux VM, make your scripts idempotent so a duplicate run during the fall-back hour doesn't corrupt data.
4. Symlinks and permissions
The cron daemon enforces strict ownership rules. Crontab files must be regular files (or symlinks to regular files) and must not be writable by anyone other than the owning user.
A common pain point in containerized deployments: crontabs loaded via symlinks. If inotify tracking is enabled, most cron daemons won't detect changes to the symlink target. You push a new config, but cron keeps running the old one.
The fix:
Send SIGHUP to force a reload:
kill -HUP $(pgrep crond)
To validate your scheduling logic before it hits production, build and test expressions with the Cron Expression Generator.