Choosing between cron and a systemd timer is not a contest between an old tool and a modern one. It is a choice about what should happen when a run is missed, a previous run is still active, the network is not ready, output has nowhere to go, or the same schedule reaches a fleet of servers. cron remains a sound fit for portable, simple wall-clock jobs. A systemd timer is usually the clearer fit for system-level work that needs dependency ordering, service state, resource controls and journal evidence. Anacron covers a narrower case: coarse daily, weekly or monthly work on machines that may be offline at the scheduled time.
Start with the job’s contract, not with syntax. If that contract is unwritten, migrating a five-field crontab line into two unit files can make the configuration longer without making the operation safer.
“Run every day at 02:00” says when an event becomes eligible. It does not say whether an offline occurrence should be discarded, delayed or replayed; whether two copies may overlap; which account and environment own the command; what must be available first; how failure is retained; or whether hundreds of hosts may start together.
Write those six decisions before selecting a scheduler:
That separation also prevents a common category error. Scheduler activation is not application completion. Voxfor’s Nextcloud background-job timing analysis shows how a healthy trigger can feed work that is not yet eligible; one finished application window matters more than a daemon that merely started a command.
The following matrix is intentionally about semantics, not popularity:
| Requirement | cron | Anacron | systemd timer |
|---|---|---|---|
| Portable five-field wall-clock schedule | Strong fit | Not its role | systemd Linux only |
| Catch up after downtime | Not by default | Yes, at day-level periods | Persistent=true for calendar timers |
| Represent every missed interval | Application must reconstruct | No; date-based overdue model | No; missed events coalesce into one activation |
| Prevent another activation while work remains active | External lock or job logic | Active-job locking | Target unit is not reactivated while active |
| Express mount, network and service dependencies | Wrapper logic | Wrapper logic | Native unit dependencies |
| Retain unit state, exit result and journal | Redirect or mail explicitly | Mail/syslog conventions | Native service state and journal |
| Spread starts across a fleet | Wrapper delay | RANDOM_DELAY is implementation-specific |
Explicit random delay/offset controls, version dependent |
Debian’s crontab(5) manual describes a daemon that checks matching fields once per minute and executes each entry as its crontab owner. That simplicity is valuable. User-owned report generation, a portable script shared with non-systemd hosts, or a short task whose own code already handles locking, logging and idempotency may need nothing more elaborate.
cron’s boundaries must still be explicit. The daemon supplies a limited environment, uses /usr/bin/sh by default in Debian’s implementation and gives % special meaning in the command field. A daemon timezone controls matching; setting TZ inside an entry changes the child environment, not necessarily the schedule itself. Output mail is useful only when a functioning mail path and an accountable reader exist.
Keep the crontab line small and move behavior into a versioned executable:
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected]
17 2 * * * /usr/local/sbin/nightly-report
Here, example.invalid is deliberately non-routable. Replace it with an owned alert path; do not assume local mail delivery is monitored. The script should emit a clear exit status, prevent unsafe overlap when necessary and record an application-level result.
According to current anacron(8) documentation, Anacron checks whether a job ran within its period measured in days. If overdue, it waits the configured delay, executes the command and writes a date timestamp after the command exits. It is designed for systems that are not continuously running.
That model works for daily package refreshes or weekly housekeeping where “sometime after the host returns” is acceptable. It is not an hourly replay engine, and it does not preserve a separate event for every day missed. Anacron’s active-job locking reduces duplicate Anacron execution, but application-side idempotency still matters when operators or other automation can invoke the same work.
A systemd timer separates eligibility from execution: example.timer activates example.service by default. Current systemd.timer(5) documentation states that an already-active target is left running rather than restarted. That makes the service lifecycle visible and naturally prevents timer-driven overlap, but it also means a long-running service can cause expected ticks to disappear rather than queue.
Pair the units so identity, filesystem access and failure behavior live with the service. Debian’s current systemd.exec(5) reference documents the execution identity and sandboxing controls available to service units:
# /etc/systemd/system/inventory-refresh.service
[Unit]
Description=Refresh local inventory
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=inventory
Group=inventory
WorkingDirectory=/srv/inventory
ExecStart=/srv/inventory/bin/refresh
NoNewPrivileges=true
PrivateTmp=true
# /etc/systemd/system/inventory-refresh.timer
[Unit]
Description=Schedule local inventory refresh
[Timer]
OnCalendar=*-*-* 02:17:00 UTC
Persistent=true
RandomizedDelaySec=20min
AccuracySec=1min
[Install]
WantedBy=timers.target
The inventory account, working directory and executable must exist with the intended ownership before this unit can run. network-online.target is not proof that a remote API is healthy; the job still needs timeouts and bounded retries. Likewise, sandboxing options must match the application. Add filesystem restrictions only after testing its real write paths.
Service state creates a useful operational boundary. When repeated failure enters a start-rate limit, systemd start-limit behavior becomes relevant; the timer itself should not be treated as the failed application.
Three details frequently decide the tool.
Persistent does not mean replay every occurrence. When Persistent=true, systemd records the last trigger time and activates the service immediately after timer activation if at least one calendar event was missed. Multiple missed calendar events produce one activation, not one activation per missed interval. If billing, telemetry or ledger work must reconstruct every interval, the application needs a durable cursor or queue.
Timer overlap is suppression, not a backlog. A matching service that remains active is not restarted when another timer event elapses. This protects many one-shot tasks from timer-origin overlap. It does not protect against manual starts, a second unit, another host or a separate scheduler. High-consequence jobs still need idempotency or a shared lock at the actual state owner.
Accuracy and random delay are different controls. AccuracySec allows systemd to coalesce local wakeups inside a window; its documented default is one minute. RandomizedDelaySec adds a random delay to spread dispatch. Newer systemd releases also add stable offset controls, but a portable unit must check the installed manual before using version-specific options. Do not copy a fleet example from current documentation into an older distribution without validation.
These distinctions matter for automation hosts. Voxfor’s GitHub Actions runner lifecycle guidance treats a runner as a persistent service with permissions and logs; scheduled cache cleanup around that runner should follow the same ownership model instead of hiding a destructive command in an anonymous crontab.
A safe review starts by discovering every possible scheduler. Duplicate ownership is more dangerous than imperfect syntax.
crontab -l
sudo crontab -l
sudo find /etc/cron.d /etc/cron.daily /etc/cron.weekly -maxdepth 1 -type f -print
systemctl list-timers --all
systemctl --user list-timers --all
Inventory container schedulers, control panels and application queues separately. A Docker health check, restart policy and scheduled business job solve different problems; Docker healthcheck and restart-policy boundaries explain why process supervision cannot substitute for an application schedule.
For each discovered job, record owner, command, schedule, last successful application result, maximum runtime, lock scope, missed-run behavior, output destination and rollback path. Application-specific completion evidence still matters; Voxfor’s WHMCS completion-marker workflow is one example of a scheduler start that needs a separate end receipt. Then use the calendar and unit-file verification modes documented in systemd-analyze(1) without waiting for production time:
systemd-analyze calendar '*-*-* 02:17:00 UTC'
sudo systemd-analyze verify /etc/systemd/system/inventory-refresh.service \
/etc/systemd/system/inventory-refresh.timer
Run the service once directly before enabling its timer. Review systemctl status inventory-refresh.service and journalctl -u inventory-refresh.service; then enable only the timer. During migration, disable the old scheduler only after the new path has produced a verified application result, and keep a documented reversal command.
Independent verification should not share the same failure domain. Voxfor’s private Uptime Kuma monitoring pattern is useful when a scheduled job produces a measurable endpoint, file age, backup object or API result. Monitor that outcome, not merely active (waiting) on a timer.
Yes. They can coexist safely when each job has one declared scheduler owner. Audit user crontabs, root crontab, /etc/cron.*, system and user timers, containers, control panels and application queues so the same work is not triggered twice.
Persistent=true run once for every missed schedule?Persistent calendar timers do not replay every missed event. systemd triggers one activation when one or more events were missed while the timer was inactive; applications that must process every interval need their own durable cursor, queue or ledger.
Timer activation does not restart its target while that unit remains active. Manual starts, another unit, another host or external automation can still create concurrency, so shared-state jobs need idempotency or locking at the state owner.
cron is reliable when skipped offline minutes, its environment, time matching and output route match the job’s contract. Problems arise when operators expect catch-up, dependency ordering or retained service state that cron does not provide by itself.
systemd exposes accuracy and randomized dispatch controls separately, making fleet intent explicit. cron can call a wrapper with a deterministic or random delay, but that delay policy, observability and retry behavior then belong to the wrapper.
Migration is unnecessary when a cron job is simple, portable, well observed and already matches the required semantics. Move it when systemd’s service ownership, dependencies, resource controls, catch-up rule or journal evidence solves a documented operational need.
The final artifact is not a prettier timer file. Keep one short record beside the job:
Choose cron when its portable skip-and-run contract already fits. Choose Anacron for coarse overdue work on intermittently available machines. Choose a systemd timer when the job benefits from an explicit service lifecycle. In every case, prove the application’s outcome independently; a scheduler can only report that it tried to start the work.