7 Crontab Mistakes That Take Down Production
Cron doesn’t page you when it’s wrong. It runs the command you gave it, on the schedule you gave it, and if that command fails silently, cron considers its job done. Every mistake below has taken down something in production somewhere, and every one of them is avoidable once you’ve seen it happen.
1. The empty-environment trap
Symptom: a script that works fine when you run it by hand fails — or silently does nothing — when cron runs it.
Root cause: cron starts each job with a minimal environment. No .bashrc, no .zshrc, no .profile, and a PATH that’s often just /usr/bin:/bin. Any binary your interactive shell finds through a customized PATH — a Node version manager, a Python virtualenv, a locally installed CLI in /usr/local/bin — cron may not find at all.
# fails silently: cron's PATH doesn't include /usr/local/bin
0 3 * * * node /home/deploy/scripts/cleanup.js
Fix: set PATH explicitly at the top of the crontab, or use absolute paths for every binary you invoke.
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * * /usr/local/bin/node /home/deploy/scripts/cleanup.js
For anything more complex, wrap the job in a script that sources what it needs explicitly rather than relying on cron to have the right environment.
2. No output capture
Symptom: a job has been failing for weeks and nobody noticed until something downstream broke.
Root cause: cron mails job output to the crontab owner by default — and on most modern servers, no mail transfer agent is configured, so that mail goes nowhere. Combined with > /dev/null 2>&1, a common (and reasonable-looking) pattern to suppress noisy output, failures become completely invisible.
# any error here vanishes — stdout and stderr both discarded
30 1 * * * /opt/backup/run.sh > /dev/null 2>&1
Fix: redirect to a log file you actually look at, or at minimum keep stderr:
30 1 * * * /opt/backup/run.sh >> /var/log/backup.log 2>&1
Better: have the job report its own outcome — write to a monitoring endpoint, push a metric, or alert on non-zero exit — so failure surfaces without anyone tailing a log by hand. This is the whole reason heartbeat-monitoring tools exist, and it’s worth adopting one even for jobs you think are too small to matter, because the ones you don’t watch are exactly the ones that fail quietly for months.
3. Overlapping runs with no locking
Symptom: a job that normally takes 90 seconds is still running from three invocations ago, and now three copies are fighting over the same resource.
Root cause: cron does not check whether the previous invocation of a job finished before starting the next one. If a job occasionally runs long — a slow query, a network hiccup, a larger-than-usual batch — cron happily starts another copy on schedule anyway. This compounds: each overlapping run adds load, which makes every run slower, which causes more overlap.
Fix: wrap the job in flock against a lock file, so a second invocation exits immediately instead of running concurrently:
*/5 * * * * flock -n /tmp/sync-inventory.lock /opt/scripts/sync-inventory.sh
-n (non-blocking) means the second invocation gives up immediately rather than queuing — usually what you want for a job that runs again in 5 minutes anyway. If you’d rather the second invocation wait for the first to finish instead of skipping, drop -n and flock will block until the lock is free — useful for jobs where every run matters and a short delay is cheaper than a skip.
Put the lock inside the script itself if the crontab line is already complex, rather than layering flock onto a long command:
#!/bin/bash
exec 200>/tmp/sync-inventory.lock
flock -n 200 || exit 1
# rest of the script
If you need only one skipped run’s work to eventually happen rather than being dropped entirely, build that into the job’s own logic (a durable queue, a “catch up since last successful run” check), not into the locking — flock prevents overlap, it doesn’t preserve skipped work.
4. Assuming the job ran because cron didn’t complain
Symptom: a report that was supposed to go out at 6am didn’t, and the crontab looks completely correct.
Root cause: cron’s silence means “I attempted to run the command,” not “the command succeeded,” and in some failure modes not even that — a syntax error in the schedule field, a typo in a path, or a crontab that failed to install correctly can mean the job never ran at all, with nothing in any log to say so.
Fix: verify with crontab -l after any edit that the job is actually present and syntactically valid — a botched edit can silently drop lines. For jobs where “did this run” matters, don’t infer it from absence of complaints; have the job actively report completion (a heartbeat ping, a last-success timestamp file, a monitoring check) so you can tell “ran and succeeded” apart from “never ran” apart from “ran and failed.”
5. Editing crontab directly on a live box, no version control or backup
Symptom: someone runs crontab -e, makes a typo, saves, and now three production jobs are gone with no record of what they used to say.
Root cause: crontab -e edits the live schedule in place. There’s no built-in undo, no diff before you commit, and depending on your editor’s exit behavior, a mistyped line can silently vanish rather than error. If nobody happened to run crontab -l > backup.txt first, that job’s old schedule is gone.
Fix: treat crontab like any other production config — keep it in version control and deploy it, rather than hand-editing on the box:
# deploy step, not a manual SSH session
crontab /repo/ops/crontab.prod
At minimum, back up before every manual edit:
crontab -l > /root/crontab.backup.$(date +%Y%m%d%H%M%S)
crontab -e
This is also where a tool that shows you a diff before writing anything to the live crontab earns its keep — the failure mode this mistake produces is specifically “I couldn’t see what I was about to change.”
6. Timezone and DST double-or-skipped runs
Symptom: a nightly job ran twice on one specific night in fall, or didn’t run at all one night in spring.
Root cause: cron runs on the system’s local time by default. Daylight Saving Time transitions create one hour that either happens twice (fall back) or doesn’t happen at all (spring forward). A job scheduled inside that hour inherits the ambiguity — a spring forward gap silently skips the run, a fall back overlap runs it twice.
Fix: pin cron-critical jobs to UTC, either at the system level or per-crontab:
CRON_TZ=UTC
0 5 * * * /opt/scripts/nightly-close.sh
If UTC isn’t practical (the job genuinely needs to run at a local wall-clock time, like “market open”), make the job idempotent — safe to run twice — so the twice-a-year double-run is a non-event rather than a data-corruption risk.
7. Jobs left on decommissioned or forgotten machines
Symptom: a report goes to an inbox nobody reads, or a cleanup job is still deleting files on a server that was supposed to be retired six months ago, or — worse — a job is still hitting a production database from a machine that was decommissioned and its credentials never rotated.
Root cause: cron jobs have no central registry. They live in whatever crontab they were written into, on whatever machine that was, and there’s no built-in way to answer “what cron jobs exist across our fleet” without SSHing into every box and running crontab -l. Machines get decommissioned, teams rotate, and the job nobody remembers writing keeps running until something it depends on breaks.
Fix: there’s no crontab-only fix for this one — it’s fundamentally a visibility problem. Before decommissioning any machine, audit crontab -l for every user on it, not just the one you remember using:
for user in $(cut -f1 -d: /etc/passwd); do
crontab -l -u "$user" 2>/dev/null && echo "^ from user: $user"
done
That still only covers one machine at a time, and it depends on someone remembering to run it before the machine goes away. Longer term, this is the strongest argument for a tool that gives you one place to see every scheduled job across every machine you own, rather than treating each crontab as its own island — which is a core part of what norc does, alongside safe writes and run history.
FAQ
Does cron log anywhere by default? Most Linux distributions log cron invocations (not output, just “job started”) to syslog or /var/log/cron — useful for confirming a job fired, not for seeing what it did.
How do I test a cron job without waiting for its schedule? Run the command exactly as written in the crontab, with the same minimal environment cron would give it — env -i /bin/sh -c '<command>' is a reasonable approximation for catching PATH-related failures before they hit production.
Should every cron job use flock? Only jobs where overlap is possible and harmful — a fast job that always finishes well within its interval doesn’t need it, but anything that talks to a network resource, database, or external API should assume it can occasionally run long.
What’s the fastest way to see every cron job on a machine? crontab -l per user, plus /etc/cron.d/, /etc/crontab, and /etc/cron.{hourly,daily,weekly,monthly} for system-level jobs — cron scheduling isn’t confined to one file.