Skip to content

cron and systemd timers

Some work on a server has to happen on a schedule rather than in response to anyone: rotating logs, renewing certificates, pruning old backups, refreshing package lists. Linux has two widely used mechanisms for this. cron is the traditional one, a small daemon that has run scheduled commands on Unix systems since the 1970s. systemd timers are the newer one: a unit type of the systemd service manager that starts another unit when a time is reached.

Both answer the same question ("run this, then") but they come from different designs, and the differences show up when something goes wrong: a job that fails silently, a laptop that was off at 03:00, a hundred servers that all hit the same mirror at midnight.

How cron works

The cron daemon (cron or crond, depending on the implementation) wakes up once a minute, works out which entries in its tables are due in that minute, and starts each one. The tables, called crontabs, are plain text with one job per line: five time fields (minute, hour, day of month, month, day of week) followed by a command.

30 2 * * *  /usr/local/bin/backup.sh

There are two kinds of crontab:

  • Per-user crontabs, edited with the crontab command and stored in a spool directory such as /var/spool/cron/. Jobs run as the user who owns the table.
  • System crontabs: /etc/crontab and the files in /etc/cron.d/, which packages and administrators drop in. These have an extra field naming the user to run each job as.

Many distributions add a convention on top: directories such as /etc/cron.daily/ and /etc/cron.weekly/, whose executable files are run in turn (usually by run-parts) from one entry in a system crontab, or by anacron. A package that needs a nightly task installs a script there and doesn't have to touch any table.

When a job is due, cron forks, switches to the job's user, and runs the command line with /bin/sh. The environment is deliberately minimal: a few variables such as HOME, LOGNAME and SHELL, a short PATH, and nothing from the user's login profile. That's the single most common reason a command "works in my shell but not from cron".

Anything the job writes to standard output or standard error is collected and, traditionally, mailed to the owner of the crontab (or to the address in MAILTO). This made sense on multi-user systems with local mail delivery. On a modern server without a mail transfer agent, that output has nowhere to go: depending on the implementation, it ends up in the system log or is discarded. Jobs that matter therefore usually redirect their own output to a log file or to logger.

Each implementation adds features on top of the classic format. The ones you will meet most often are:

Implementation Where you meet it Notable features
ISC/Vixie cron (cron package) Debian and Ubuntu The classic implementation, with many Debian patches
cronie RHEL and its rebuilds, Arch, the first choice on Gentoo Vixie cron's successor; CRON_TZ, RANDOM_DELAY, inotify; ships anacron
fcron Arch, Gentoo Can run jobs that were missed while the machine was off; per-job options
dcron, bcron Gentoo Smaller alternatives with fewer extensions
systemd-cron Debian, Gentoo No daemon: a generator turns crontabs into systemd timers

How systemd timers work

A systemd timer is a unit file with the suffix .timer. It contains no command. Instead, it says when to start another unit, by default the service of the same name: backup.timer starts backup.service. The service unit holds the command, along with everything systemd can say about how to run a process.

# backup.timer
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target

Timers come in two flavours, which can be mixed in one unit:

  • Realtime (wall-clock) timers, with OnCalendar=, fire at calendar times: every day at 02:30, every Monday, the first of each month. Their syntax is richer than cron's five fields: seconds, years, ranges with steps, and time zones are all expressible, and systemd-analyze calendar shows when an expression will next elapse.
  • Monotonic timers fire a time span after an event: 15 minutes after boot (OnBootSec=), an hour after the service last started (OnUnitActiveSec=). They don't care what time it is, which makes them a natural fit for "every so often" jobs whose exact time doesn't matter.

systemd itself keeps track of every timer; there is no separate daemon. systemctl list-timers shows all of them, when each one last fired and when it will fire next, which has no direct equivalent in cron.

Trade-offs

Logging and failure reporting

This is the biggest practical difference. A timer's job runs as an ordinary service, so its output goes to the journal, tagged with the unit name, and its exit status is recorded: systemctl status backup.service tells you whether the last run succeeded, and journalctl -u backup.service shows what it printed, run by run. A failed run leaves the unit in the failed state, which monitoring can pick up, and OnFailure= in the service can start another unit to send an alert.

cron knows only that it started a command. It logs that it did so, but whether the command succeeded, and what it printed, is the job's own business unless mail delivery is set up. Well-run cron jobs compensate with explicit logging, but each job has to do it.

Execution environment and dependencies

Because the job is a service, everything a unit file can express is available: run as a dedicated user, set the working directory and environment explicitly, limit memory or CPU, restrict access to the filesystem, wait for the network or a mount with After= and Requires=, or skip the run when a condition isn't met. In cron, the environment is fixed and minimal, and anything else must be done inside the command.

A timer also never starts a second copy of a job that is still running: if the service is still active when the timer elapses again, systemd doesn't start it a second time. cron starts a new copy every time the schedule matches, so a nightly job that one night takes 25 hours ends up running twice at once unless it takes a lock itself (for example with flock).

The cost of all this is verbosity. A cron job is one line; a timer is two files, a daemon-reload and an enable. For a quick personal job on a workstation, cron is hard to beat.

Missed runs

cron runs a job only if the daemon is running at the minute the schedule names. If the machine is off or asleep at 02:30, the 02:30 job doesn't run that day, and nothing records that it was missed. That's fine for servers that are always on, and a real problem for laptops and for servers that were down for maintenance.

anacron was written to fill that gap. It doesn't run at precise times; instead it records when each job last ran and, when it's started, runs any job whose period (in days) has passed. On systems with cronie (RHEL and its rebuilds, Arch, and Gentoo with cronie's default anacron USE flag), cronie's anacron runs the /etc/cron.daily/, weekly and monthly directories this way, started once an hour from cron. anacron works at the granularity of days and is normally run as root.

systemd timers handle this with Persistent=true: systemd stores the time each persistent calendar timer last fired, and when the timer starts (typically at boot) it runs the job straight away if an elapse was missed while it was inactive. Monotonic timers relative to boot don't need this, because they count from the boot itself.

Spreading the load

If every server in a fleet runs its update check at exactly 00:00, they all hit the same mirror or API at once. Classic cron has no built-in remedy; administrators either hard-code a different minute per host or start each job with a random sleep. cronie added a RANDOM_DELAY variable for this, and anacron has a similar setting.

Timers have RandomizedDelaySec=, which adds a random delay up to the given span to each elapse, and FixedRandomDelay=, which picks a delay derived from the machine and unit and keeps it the same on every run. Many timers that distributions ship, such as the ones that refresh package metadata, use a randomised delay for exactly this reason.

Timers also have the opposite setting, AccuracySec=, which allows systemd to delay an elapse by up to the given span (one minute by default) so that it can wake the system once for several timers. That's why a timer can fire some seconds after the time you set.

Per-user jobs

Any user allowed by /etc/cron.allow and /etc/cron.deny can run crontab -e and have jobs that run whether or not they are logged in. systemd has user timers too (systemctl --user), but they run in the user's own service manager, which by default exists only while the user has a session; to run user timers without a login, an administrator has to enable "lingering" for that user with loginctl enable-linger. For system tasks this difference doesn't matter; for shared machines where users schedule their own jobs, cron is still the simpler model.

Which one distributions use

On the systemd distributions covered here, the distributions' own scheduled work has largely moved to timers: apt-daily.timer on Debian and Ubuntu, fstrim.timer, logrotate.timer, man-db.timer, systemd-tmpfiles-clean.timer and others. Whether a cron daemon is present at all depends on how the system was installed. In the November 2026 cloud images used for this site's labs, Ubuntu 26.04 has ISC cron installed and running, AlmaLinux 10 and Rocky Linux 10 have cronie with anacron, and Debian 13 and Arch have no cron daemon at all.

Gentoo with OpenRC

OpenRC is a service manager that starts and stops services; it has no timer units, so scheduled jobs on an OpenRC system are cron's job. Gentoo doesn't install a cron daemon by default and leaves the choice to the administrator. The packages that satisfy Gentoo's virtual/cron are sys-process/cronie (the first choice listed), sys-process/bcron, sys-process/dcron, sys-process/fcron and sys-process/systemd-cron, the last of which is useful only with a systemd profile. Whichever one is installed, it runs as an OpenRC service that has to be added to the default runlevel, like any other daemon.

A Gentoo system without a cron daemon can still have files in /etc/cron.daily/ and /etc/cron.weekly/, installed by packages such as man-db and mdadm; they simply never run until a cron daemon is installed.

On a Gentoo system with a systemd profile, timers work exactly as on any other systemd distribution.

Further reading