Cron Is a Time Machine You Already Own

Contents
Cron turned fifty this decade, give or take, and it hasn’t changed in any way that matters. The five-field time specification, the crontab file, the “run this command at this moment, unattended” model — all of it is functionally identical to the version Brian Kernighan and colleagues at Bell Labs were running on Unix in the late 1970s. Every few years someone builds a slicker scheduler with a web dashboard and a YAML config format, and cron keeps running anyway, on more machines than any of its replacements combined, because the actual problem it solves has never once changed: I want a thing to happen at a specific time, and I don’t want to be the one who has to remember to trigger it.
That’s the part worth sitting with. Cron is, functionally, a time machine, in the specific and useful sense of letting you delegate an action to a future moment and then stop thinking about it. You decide once, today, that a backup should run at 2am every night, and from that point forward the decision executes itself indefinitely, without you being awake, without you remembering, without the decision ever needing to be made again. Most of what people call “automation” in a homelab is really just cron jobs pointed at scripts, dressed up with a nicer word.
Why the five-field syntax has survived unchanged
The crontab line format looks arcane the first time you see it, and then becomes completely transparent once you understand it isn’t clever, it’s just positional.
| |
Five fields, always in the same order, each accepting a number, a range, a step (*/15), a list, or a wildcard meaning “any value.” That’s the entire grammar. It’s survived fifty years because there’s genuinely very little to improve — almost any schedule a person actually needs (“every night at 2am,” “every fifteen minutes,” “the first of the month,” “weekdays only”) maps cleanly onto five fields, and the rare schedule that doesn’t (like “every second Tuesday”) is better handled in the script itself than by contorting the syntax to express it.
Where cron actually lives on a modern system
Cron is really a daemon (cron or crond) reading a set of text files, and understanding where those files live explains a lot of confusing behaviour around permissions and ordering.
- Per-user crontabs, edited with
crontab -e, run as that specific user with that user’s environment and permissions. This is where most personal automation belongs, because the job inherits exactly the access the user account already has, nothing more. /etc/crontabis a system-wide file with an extra field for which user to run as, typically used for jobs that genuinely need to run as a specific service account rather than whoever happens to be logged in./etc/cron.d/holds one-file-per-package system crontabs, which is how most Linux packages that need scheduled behaviour (log rotation, certificate renewal checks) install their jobs without touching a shared file another package might also be editing./etc/cron.daily,/etc/cron.hourly,/etc/cron.weeklyare directories of executable scripts, run by a wrapper job (run-parts) at the implied frequency — this is the mechanism behind a lot of “it just runs on its own” system maintenance you never configured yourself.
Most cron confusion traces back to a job living in the wrong one of these, running as the wrong user, or being edited in the wrong place entirely.
What cron doesn’t do, and why that’s the actual design
The most common complaint about cron is a category error: people expect it to notify them when a job fails, and it doesn’t, because that was never its job. Cron’s entire responsibility ends at “run this command at this time.” Whether the command succeeds, whether it takes ten seconds or ten hours, whether it silently fails halfway through — none of that is cron’s concern, and building failure notification into cron itself would have made it a much larger, much more fragile piece of software solving a problem most jobs don’t actually have.
This is why the modern cron ecosystem looks the way it does: cron stays small and does one thing, and a whole category of tools exists specifically to answer “did the cron job I set up last month actually run?” A dead man’s switch service, pinged at the end of a successful script run, will alert you the moment a scheduled ping doesn’t arrive on schedule — which is the only reliable way to know a job silently stopped running, since a cron job that fails to fire produces no error, no log entry, and no signal of any kind by default.
| |
The systemd timer argument, and why I still reach for cron first
Every few years someone points out, correctly, that systemd timers can do everything cron does and more — dependency ordering, resource limits via cgroups, structured logging through the journal, and the ability to catch up on a missed run after a machine’s been asleep or powered off, which cron simply cannot do at all. All of that is true, and for a job with real dependencies (don’t start the backup until the database container is confirmed healthy) or a job on a machine that isn’t always on, a timer unit is the better tool, full stop.
But for the overwhelming majority of scheduled tasks in a typical homelab — a nightly backup, a certificate renewal check, a log rotation — none of that extra capability is being used, and the cost of a timer unit is real: two files instead of one line (a .timer and a paired .service), a heavier syntax to get right, and a debugging story that runs through systemctl status and journalctl rather than a single crontab you can read top to bottom in ten seconds. I use both in different places, but the decision is deliberate rather than reflexive: cron for anything that’s genuinely just “run this at this time” with no dependency on another service’s state, systemd timers for anything that needs to react to the system actually being ready first.
| |
The Persistent=true line is the one genuine capability cron cannot replicate at all: if the machine was off at 2am, the timer fires the missed job as soon as the system is back up, which matters enormously for a laptop or a machine that isn’t left running continuously. For an always-on server, that distinction rarely matters, and the extra file and syntax stop paying for themselves.
A backup job that actually needs cron’s simplicity
The clearest case for cron over anything more elaborate is a script that’s already doing its own orchestration internally and just needs a trigger, not a scheduler with opinions about how the job should run.
| |
Everything about failure handling, logging and success confirmation lives inside the script itself, which is precisely why it doesn’t need a scheduler with built-in retry logic or dependency graphs — the script already knows what “success” means for this specific job, and cron’s only responsibility is showing up at 2am and running it. Pushing that logic into the scheduler instead of the script is usually a sign the wrong tool got picked, not that the scheduler needs more features.
Troubleshooting the classic cron failure modes
Nearly every “cron doesn’t work” report traces back to one of a small, well-known set of causes, because cron’s environment is deliberately minimal and nothing like an interactive shell.
“It works when I run it manually but not from cron.” This is almost always a PATH problem. Cron runs jobs with a minimal environment, not your shell’s full profile, so a script that calls python3 or docker by bare name, relying on your interactive shell’s PATH, will fail silently under cron where that PATH doesn’t exist. Always use absolute paths inside cron-triggered scripts, or explicitly set PATH at the top of the crontab.
No error output anywhere. By default, cron emails job output to the owning user’s local mailbox, which on most modern systems either isn’t configured or isn’t checked. Redirect output explicitly instead: >> /var/log/myjob.log 2>&1 at the end of the crontab line, so failures leave a trail you’ll actually see.
Job runs at the wrong time. Check the system timezone the cron daemon is actually using, not the timezone you assume the server is in — a server provisioned with UTC as its system timezone will run a 0 2 * * * job at 2am UTC, not 2am local time, and this catches people constantly on cloud instances provisioned in a different region to where they live.
Job silently stops running after a system update. Some package managers reset or overwrite /etc/cron.d/ entries during upgrades, or a permissions change on the crontab file itself (cron is strict about file ownership and mode) can cause the daemon to silently ignore a file it used to read. Check journalctl -u cron for lines about ignoring a crontab due to bad permissions.
Two jobs overlap and interfere with each other. Cron will happily start a new run of a job at the scheduled time even if the previous run of that same job hasn’t finished, and a long-running backup that occasionally takes longer than its interval can end up with two copies fighting over the same files. Wrapping the command in flock against a lock file (flock -n /tmp/backup.lock /usr/local/bin/backup.sh) fixes this permanently, and it’s worth doing for any job whose runtime can vary.
One more thing that catches people out: user versus root crontabs
A single machine can have several completely independent crontabs active at once, one per user account plus the system-wide ones, and it’s extremely common to edit the wrong one without realising it. Running crontab -e edits the crontab for whichever user you’re currently logged in as — if you sudo into root to edit a job, you’re editing root’s crontab, entirely separate from your own user’s, and a job placed there runs with root’s permissions and root’s environment entirely. This distinction matters most when a script that works fine when you test it manually as your own user suddenly behaves differently once scheduled, because it’s now running as a different account with different file permissions, a different home directory, and potentially a different PATH again on top of cron’s already-minimal one. Checking crontab -l -u root versus crontab -l for your own account is often the fastest way to find a job that “should” be running but apparently isn’t — it’s frequently sitting in the wrong user’s file entirely, quietly correct and quietly ignored.
Is it worth using in 2026
For anything running on a single machine — backups, certificate renewal checks, log rotation, health pings — cron remains the correct default, not the legacy option you graduate away from. It has no dependencies, no daemon to keep patched beyond what your distribution already maintains, and a failure mode simple enough to reason about in your head. Where it stops being enough is genuinely distributed scheduling across a fleet of machines, or jobs that need retries, dependencies between steps, or a dashboard — that’s a real gap, and it’s what pushed me toward Kubernetes CronJobs for the handful of jobs in my homelab that actually need that. But wiring a dead man’s switch onto an existing cron job, via a self-hosted health-check service, closes the one real gap in cron’s design without replacing fifty years of boring, dependable behaviour with something that has its own new failure modes to learn.




