A crontab edited with crontab -e persists across reboots. It is written to a file under /var/spool/cron and read by the cron daemon on every start. If your scheduled job disappeared after a restart, it was almost certainly not in a crontab — it was in a container, a temporary file, or a shell session that ended.
That is the useful reframing. Cron persistence is not something you configure; it is the default. What varies is whether the thing you edited was a real crontab, and whether the environment it lives in survives at all.
Table of contents
- Where crontabs actually live
- Why jobs actually disappear
- The environment problem
- Seeing what happened
- Missed runs while the machine was off
- When to use systemd timers instead
- Scheduled work in a deployed application
- How this fits the rest of the stack
- FAQ
Where crontabs actually live
Several distinct mechanisms all get called cron, and they persist differently:
- User crontabs — written by
crontab -eto a file under /var/spool/cron or /var/spool/cron/crontabs. Owned by a user, persist across reboots, backed up only if you back up that directory. - The system crontab — /etc/crontab. Same format plus a user field. Persists, and is a normal file you can put in configuration management.
- Drop-in files — /etc/cron.d/. One file per concern, same format as /etc/crontab. This is the best place for jobs installed by a package or deployment.
- Directory-based — /etc/cron.hourly, cron.daily, and friends. Scripts dropped in, run on that cadence.
All of them persist. The reason to prefer /etc/cron.d for anything deployed is that it is a file on disk in a predictable location, so it can be version controlled and written by a deployment, whereas a user crontab is state that someone edited interactively.
The rule worth adopting: if a job matters, it should be a file you can see in a repository, not something typed into an editor once.
Why jobs actually disappear
Given that persistence is the default, a vanished job usually has one of these causes:
- It was in a container. Containers are recreated from images. Anything written to the filesystem at runtime — including a crontab — is gone on recreation. This is by far the most common cause now.
- The crontab was edited by writing directly to the spool file rather than through crontab -e. Some cron daemons cache and overwrite, so a directly-edited file gets clobbered.
- A configuration management tool reverted it. If the machine is managed, anything not in the configuration is drift and gets removed on the next run. Working as designed.
- It was scheduled with
atrather than cron. The at command schedules a one-off and does not repeat, which looks like disappearance after it runs. - A different user’s crontab. crontab -e edits the crontab of whoever runs it. Adding a job as root and looking for it as your normal user finds nothing.
The container case is worth expanding because the fix is architectural. Putting cron inside a container works, and it means the container runs two things — your process and a scheduler — which complicates signal handling, logging, and restart behaviour. The cleaner shapes are a scheduler outside the container that invokes it, or a platform-level scheduled task.
The environment problem
The other classic failure is not disappearance but silence: the job is definitely there and never appears to run. Nine times in ten this is the environment.
Cron runs jobs with a minimal environment. PATH is typically just /usr/bin:/bin — not your login PATH. So a command that works in your shell fails in cron because the binary is not found.
Nothing you have set up in .bashrc or .profile exists either. No language version manager, no virtualenv, no exported credentials.
Two fixes. Set what you need at the top of the crontab:
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
0 3 * * * /usr/local/bin/python3 /opt/app/job.py
Or, better, use absolute paths for everything and have the job source what it needs. A wrapper script is usually the cleanest answer:
#!/bin/bash
set -euo pipefail
cd /opt/app
source .env
exec /opt/app/venv/bin/python job.py
Then the crontab line is just the wrapper. The environment setup lives in a file you can run by hand to test, which is the property that makes this debuggable.
Seeing what happened
Cron mails output to the user by default, and on most systems mail is not configured, so output goes nowhere. That is why jobs seem to run silently.
Redirect explicitly:
0 3 * * * /opt/app/job.sh >> /var/log/myjob.log 2>&1
The 2>&1 is the important part — without it you capture standard output and lose the errors, which are exactly what you need.
To confirm cron is even firing the job, check the system log. On systemd-based systems:
journalctl -u cron --since today
# or, depending on distribution
journalctl -u crond --since today
That shows cron starting each job. If the job appears there and produced no output, it ran and failed early — usually the PATH problem. If it does not appear at all, the schedule or the crontab is wrong.
One more thing that catches people: a crontab file must end with a newline. Some cron implementations silently ignore a final line without one, so the last job never runs while every other one does.
Missed runs while the machine was off
Cron has no memory. If a job was scheduled for 3am and the machine was off at 3am, the job does not run — not then, and not on the next boot.
For servers that stay up this is irrelevant. For laptops, workstations, and machines that get restarted, it matters, and there are two answers.
anacron runs jobs on a daily, weekly, or monthly cadence and catches up after downtime. It tracks when each job last ran and runs it shortly after boot if the interval has elapsed. The trade is that you cannot specify a precise time — anacron guarantees cadence, not schedule.
systemd timers with Persistent=true do the same thing with more control:
[Unit]
Description=Nightly cleanup
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Persistent=true records the last run and fires immediately after boot if it was missed. RandomizedDelaySec spreads the load when many machines would otherwise fire simultaneously — genuinely useful if the job hits a shared service.
When to use systemd timers instead
For anything beyond a trivial job, timers are better, and the reasons are practical rather than ideological:
- Logging is automatic. Output goes to the journal, queryable with journalctl -u yourjob. No redirect, no lost output.
- Missed runs are handled with Persistent=true, which cron cannot do.
- Overlap is prevented. A service unit will not start a second instance while the first is running. Cron happily starts a second copy of a job that overran, which is how jobs pile up.
- Dependencies are expressible. After= and Requires= let a job wait for the network or a database.
- Resource limits apply, since it is a normal service unit with memory and CPU controls available.
The cost is two files instead of one line and a syntax that is more verbose. For a one-line cleanup, cron is fine. For anything whose failure you would want to know about, the timer pays for itself the first time it overlaps or fails.
That overlap point deserves emphasis. A cron job that normally takes two minutes and occasionally takes twenty, scheduled every five minutes, will eventually have four copies running at once. Cron will not stop it, and the resulting resource exhaustion looks like a mystery until you count the processes.
Scheduled work in a deployed application
For an application deployed from a repository, the underlying question changes. There may be no persistent machine to hold a crontab, and if the service scales to several instances, a crontab on each one means the job runs several times.
That last problem is the one that causes damage — a nightly billing job running three times because three instances each had a scheduler. The shapes that avoid it:
- A single dedicated worker that is the only thing running scheduled work, separate from the instances serving traffic.
- A lock in shared storage, so whichever instance fires first claims the job and the others skip it. A row in the database with a unique constraint on the job name and scheduled time works.
- An external scheduler that calls an endpoint on the service, so the schedule lives in one place regardless of how many instances exist.
Whichever you choose, the property worth preserving is the one cron gives you for free on a single machine and takes away the moment there are two: exactly one execution per scheduled time.
How this fits the rest of the stack
Scheduled work is one of those things that is trivial on one long-lived server and needs actual thought the moment there is more than one instance, because cron’s guarantee of a single run per machine becomes one run per instance. Deploying from a repository with runtime logs means a job’s output is in the same place as the requests it ran alongside, rather than in a file you have to remember to redirect to. Services on RunxBuild covers that, and if you are sizing a worker alongside the service and database it operates on, the RunxBuild hosting calculator shows them as separate line items.
Useful related references:
- Kubernetes Persistent Volume: PV, PVC, StorageClass, and Dynamic Provisioning
- Syncthing Docker: Persistent Volumes, Ports, Permissions, and Safe Remote Access
- Persistent Storage in 2026: When Your App Needs to Outlive Its Container, and How to Stop Finding Out at 3 a.m.
- Docker services on RunxBuild
FAQ
Do crontab entries survive a reboot?
Yes. User crontabs are stored on disk under /var/spool/cron and reloaded by the cron daemon on every start. A job that vanished was almost certainly in a container, a directly-edited spool file, or a configuration-managed machine that reverted it.
Why does my cron job not run even though it is in the crontab?
Usually the environment. Cron runs with a minimal PATH and none of your shell profile, so commands that work interactively fail. Use absolute paths, or wrap the job in a script that sets up its own environment.
How do I see cron job output?
Redirect it explicitly with >> /var/log/myjob.log 2>&1 — the 2>&1 captures errors, which is what you actually need. To confirm cron fired the job at all, check journalctl -u cron.
What happens to a cron job if the machine is off at the scheduled time?
It is skipped entirely and does not run on the next boot. Use anacron for daily or weekly cadence with catch-up, or a systemd timer with Persistent=true for missed-run handling with precise scheduling.
Should I use cron or systemd timers?
Timers for anything whose failure matters — they log to the journal automatically, handle missed runs, and prevent a second instance starting while the first still runs. Cron is fine for trivial one-line jobs.