Why I Stopped Using crontab -e
For years I managed cron jobs the same way everyone does — SSH into the server, run crontab -e, add a line, save, done. Quick, easy, impossible to track.
Then I lost a crontab.
A colleague ran crontab -r instead of crontab -e on a production server. Wiped out 14 scheduled jobs. We had no backup, no record of what was in there. Spent the rest of the day piecing it together from deploy scripts, wiki pages, and Slack messages from six months ago.
That was the last time I used crontab -e for anything that matters.
What I do instead
Every cron job now lives in a file, checked into git, deployed with the rest of the code.
The setup is dead simple. Drop your cron definitions into a file:
/etc/cron.d/myapp-jobs
The format is the same as a regular crontab, except you specify the user:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
*/5 * * * * deploy /app/scripts/sync-data.sh >> /var/log/sync.log 2>&1
0 3 * * * deploy /app/scripts/nightly-backup.sh >> /var/log/backup.log 2>&1
0 */6 * * * deploy /app/scripts/cleanup-temp.sh >> /var/log/cleanup.log 2>&1
That file gets deployed alongside your application code. Version controlled, reviewed in PRs, rollback-able. If someone deletes it accidentally, git checkout brings it back in seconds.
The things that bit me before
No output redirection. Half our old crontab entries didn't have 2>&1. Errors went to local mail that nobody read. Jobs failed for weeks without anyone knowing.
Missing PATH. Cron's default PATH is basically just /usr/bin:/bin. Every script that called node, python3, or anything installed via a package manager would fail silently. Setting PATH at the top of the cron file fixes this once for all jobs.
No logging. "Did the backup run last night?" shouldn't require SSH and detective work. Every job now logs to its own file with timestamps. Takes five seconds to check.
The pattern I settled on
Each script follows the same structure:
#!/bin/bash
echo "$(date): Starting backup"
/app/scripts/backup.sh
EXIT_CODE=$?
echo "$(date): Finished with exit code $EXIT_CODE"
if [ $EXIT_CODE -eq 0 ]; then
curl -fsS --retry 3 https://monitor.example.com/ping/backup-job
fi
That last part — the ping on success — changed everything for me. I use WatchCron to track these heartbeats. If a ping doesn't arrive within the expected window, I get an alert. No more "the backup hasn't run in five days and nobody noticed."
The beauty of this pattern is what it doesn't require. No agent installed on the server. No special integration. No daemon running alongside your app. Just a curl at the end of your script.
What about systemd timers?
Yeah, they're technically superior. Better logging, dependency management, proper service integration. I use them for complex jobs that need those features.
But for the typical web app cron job — run this script every N minutes, log the output, alert if it fails — a cron file in /etc/cron.d/ is simpler and more portable. Not every server runs systemd. Every server runs cron.
Pick the tool that matches the complexity of the job. A nightly database backup doesn't need a systemd unit file with twelve directives.
The migration
If you're sitting on a bunch of jobs in crontab -e right now, migrating takes about 20 minutes:
- Dump current crontab:
crontab -l > current-crontab.txt - Review each line — half of them are probably outdated or broken anyway
- Move the valid ones into
/etc/cron.d/yourapp - Add PATH and output redirection to each one
- Add a heartbeat ping to the critical ones
- Commit the file, deploy it, verify jobs run
The hardest part is step 2. You'll probably discover jobs that nobody remembers adding, jobs that reference scripts that no longer exist, and jobs that duplicate each other. Good. Better to find that now than during an incident.
After the migration, crontab -l should return empty. All your jobs are in version control where they belong. And if someone accidentally runs crontab -r again, nothing happens.


