Cron vs systemd Timers: Which Should You Use for Scheduled Linux Jobs?
Linux servers quietly depend on scheduled jobs.
They may:
create backups
rotate reports
clean temporary files
synchronise data
renew certificates
run maintenance
generate exports
For decades, the standard answer was cron.
On modern Linux distributions using systemd, you also have systemd timers.
Both can run something at a particular time.
But they solve the problem differently.
The simplest distinction is:
cron asks: “When should I run this command?”
systemd asks: “When should I activate this managed service?”
That extra service-management layer makes systemd timers more verbose, but it also gives you better control over logging, dependencies, missed runs, timeouts and failure handling.
The Short Answer
Use cron when:
the task is simple
portability matters
the machine is normally running
missing one run is acceptable
a one-line schedule is easier to maintain
Use a systemd timer when:
missed jobs should catch up later
failure visibility matters
the task depends on another service, network or mount
you need explicit timeouts
you need tighter security controls
the task is operationally important
you manage many similar Linux systems
Neither is automatically better.
The important question is:
What should happen when the job does not run exactly as planned?
What Is Cron?
Cron is the traditional Unix/Linux time-based scheduler.
A typical crontab entry might be:
30 2 * * * /usr/local/bin/backup.sh
That means:
run /usr/local/bin/backup.sh every day at 02:30.
The five fields are:
minute hour day-of-month month day-of-week
For example:
0 6 * * 1-5 /home/user/bin/report.sh
means:
06:00 Monday to Friday.
Cron's greatest strength is its simplicity.
For a straightforward job, one line can be all you need.
Where Does Cron Configuration Live?
A user can edit their crontab using:
crontab -e
and inspect it with:
crontab -l
System-wide schedules may also exist under locations such as:
/etc/crontab
/etc/cron.d/
/etc/cron.daily/
/etc/cron.weekly/
Exact behaviour varies slightly between distributions.
One of the first rules when troubleshooting cron is therefore:
make sure you are looking at the correct user's crontab.
A job configured for root will not appear in your ordinary user's crontab.
Cron's Environment Is Smaller Than Your Shell
This causes a huge number of failures.
A command works perfectly when you type:
backup-tool --run
into an interactive shell.
Cron runs it and nothing happens.
Why?
Because cron usually runs with a much more limited environment.
It may not have the same:
PATH
shell profile
environment variables
working directory
So instead of:
0 2 * * * backup-tool --run
prefer explicit paths:
0 2 * * * /usr/local/bin/backup-tool --run
And inside scripts, avoid assuming the current directory.
Explicit paths make scheduled jobs considerably easier to diagnose.
Cron Logging Needs Deliberate Design
A common cron entry is:
0 2 * * * /usr/local/bin/backup.sh
But where does its output go?
Depending on configuration, cron may:
email output
write via system logging
effectively lose output nobody checks
For important jobs, capture output deliberately:
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
That gives you something to investigate later.
But remember to manage the logfile too.
A backup job that works perfectly for three years while its log eventually fills the filesystem is not ideal automation.
Cron and Missed Jobs
Suppose a laptop is powered off at:
02:30
when this job should run:
30 2 * * * /usr/local/bin/backup.sh
With ordinary cron, that occurrence is simply missed.
Cron does not normally say:
> “The machine was off at 02:30, so I'll run it at 08:05 when it starts.”
That behaviour can be perfectly acceptable for:
temporary cleanup
non-critical reports
tasks that will simply run again tomorrow
It is much less attractive for:
backup
billing
security maintenance
where every scheduled run matters.
Traditionally, tools such as anacron helped cover that gap.
On systemd systems, persistent timers provide a cleaner integrated option.
Cron Has Some Scheduling Surprises
Consider:
30 4 1 * 5 /usr/local/bin/report.sh
You might interpret that as:
04:30 when the date is both the first of the month and a Friday.
Traditional cron semantics generally treat restricted day-of-month and day-of-week fields as an OR.
So the job can run:
on the first day of the month
and every Friday
at 04:30.
For unusual schedules, validate your expression rather than assuming it means what it visually appears to mean.
Daylight Saving Time Can Matter
Wall-clock schedules can behave strangely around daylight-saving transitions.
If the clock jumps forward, a scheduled local time may never exist.
If it moves backwards, an hour may occur twice.
For:
housekeeping
temporary cleanup
that may not matter much.
For:
billing
financial processing
notifications
compliance jobs
it may matter considerably.
That is another reason business-critical scheduling deserves more design than simply picking a cron expression.
What Is a systemd Timer?
A systemd timer normally consists of two units.
A service describes the work.
A timer describes when the service should run.
For example:
/etc/systemd/system/server-backup.service
[Unit]
Description=Server backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Then:
/etc/systemd/system/server-backup.timer
[Unit]
Description=Run server backup nightly
[Timer]
OnCalendar=02:30
Persistent=true
[Install]
WantedBy=timers.target
Enable it with:
sudo systemctl enable --now server-backup.timer
Now the schedule and the actual workload are separated cleanly.
Why Separate the Timer and Service?
At first, this looks like a disadvantage.
Cron needed:
one line.
Systemd needed:
two files.
But the service unit gives systemd somewhere to define:
user
working directory
environment
dependencies
security restrictions
timeouts
resource controls
restart/failure behaviour
The timer only has to answer:
When should this service run?
For important server workloads, that separation is extremely useful.
OnCalendar= Is the Cron-Like Schedule
For wall-clock schedules:
OnCalendar=*-*-* 02:30:00
means every day at 02:30.
You can test a calendar expression before installing it:
systemd-analyze calendar '*-*-* 02:30:00'
This is one of my favourite systemd timer features.
It will show you the next scheduled occurrences, reducing the chance of deploying an expression that does not mean what you think it means.
Systemd Can Also Schedule Relative to Events
Systemd timers are not limited to calendar times.
Examples include:
OnBootSec=10min
Run ten minutes after boot.
OnUnitActiveSec=1h
Run an hour after the previous activation.
OnUnitInactiveSec=30min
Schedule relative to when the service became inactive.
This is useful for jobs where:
every X hours
matters more than:
at exactly 02:30.
Persistent Timers Can Catch Up Missed Runs
This is one of the biggest advantages over ordinary cron.
Add:
Persistent=true
to an OnCalendar= timer.
If the scheduled run was missed while the machine was off, systemd records the previous trigger state and can activate the service after the system becomes available again. The systemd timer documentation explicitly describes Persistent= as a mechanism for catching up calendar timers that elapsed while inactive.
That makes it useful for:
laptops
occasionally powered servers
maintenance jobs where missed execution matters
It does not mean systemd will replay every missed occurrence individually.
It means the timer can trigger because at least one scheduled occurrence was missed.
Systemd Logging Is Much Better Integrated
Run:
systemctl status server-backup.service
to see the latest state.
Then:
journalctl -u server-backup.service
to inspect its logs.
Or:
journalctl -u server-backup.service --since today
This is far cleaner than wondering:
Did cron run? Where did stdout go? Did anyone receive the email?
For production jobs, centralised service status is one of the strongest arguments for timers.
Listing Timers Is Easy
Run:
systemctl list-timers
You can see:
timer name
last activation
next activation
time remaining
That gives administrators one place to inspect scheduled systemd work.
For a specific timer:
systemctl status server-backup.timer
Dependencies Are a Major Advantage
Suppose a scheduled backup requires:
network connectivity
mounted storage
database service
With cron, your script usually has to test those conditions itself.
Systemd can model some of them directly.
For example:
[Unit]
RequiresMountsFor=/mnt/backup
After=network-online.target
Wants=network-online.target
That does not magically guarantee the internet is functioning perfectly.
But it gives the scheduler knowledge about service ordering and required resources that cron does not inherently possess.
Systemd Gives You Better Timeouts
Imagine a backup normally takes:
10 minutes
but one day hangs indefinitely.
A service can define:
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
TimeoutStartSec=1h
That stops one stuck job from remaining around forever.
You can also define resource and security restrictions without stuffing them into the script itself.
Preventing Overlapping Runs
Suppose a job starts hourly but occasionally takes more than an hour.
You do not necessarily want:
run 1 still active
plus:
run 2
plus:
run 3
stacking up.
Systemd service state already helps here because a service that is still active will not simply start a second identical instance in the ordinary case.
For more specialised calendar jobs, newer systemd also provides:
DeferReactivation=true
for OnCalendar= timers. This schedules the next activation based on the service becoming inactive, helping avoid immediate reactivation when the service itself took longer than the configured interval. This option was added in systemd 257.
That is particularly useful for long-running maintenance jobs.
Randomising Timer Execution Across Many Machines
Imagine 500 laptops all run:
backup at 09:00 Monday
Your backup service suddenly receives 500 clients at once.
Systemd supports:
RandomizedDelaySec=30min
which spreads timer activations over an interval instead of firing them all simultaneously.
That is useful for:
backups
update checks
inventory
fleet maintenance
But there is an even newer option worth knowing about.
RandomizedOffsetSec= Is Better for Stable Fleet Scheduling
Recent systemd versions add:
RandomizedOffsetSec=
for calendar timers.
Unlike an ordinary random delay that is recalculated around manager startup, RandomizedOffsetSec= creates a stable per-machine offset and preserves the schedule across reboots.
The systemd manual gives almost exactly the business use case you would expect: spreading a fleet of weekly backups across different times without losing the periodic schedule when machines restart. It was introduced in systemd 258.
So for a modern managed fleet, this can be better than:
RandomizedDelaySec=5d
when you need each client to keep a consistent weekly slot.
Check your distribution's systemd version before using newer directives.
Cron Is More Portable
Cron still wins easily on portability.
It exists across:
many Linux distributions
BSDs
other Unix-like systems
Systemd timers require:
systemd.
If you maintain scripts that need to work across mixed Unix environments, cron can be a more practical common denominator.
That simplicity is not obsolete.
It is a feature.
Cron Is Also Easier for Very Small Jobs
Suppose you want:
15 3 * * * /usr/local/bin/tmp-cleanup
The job:
runs quickly
is not business-critical
does not need catch-up
does not need dependencies
has trivial failure impact
Writing two systemd units may be needless complexity.
Use cron.
Good engineering is not about choosing the more sophisticated technology every time.
User systemd Timers
Systemd timers do not need to be system-wide.
Users can create timer/service units under:
~/.config/systemd/user/
and manage them with:
systemctl --user
For example:
systemctl --user list-timers
This can be a useful alternative to user crontabs.
But user services have different lifecycle/session considerations from system services, so do not assume they behave identically when no user session exists.
For server-wide operational jobs, system units are generally easier to reason about.
Cron vs systemd: A Practical Comparison
Requirement Cron systemd timer
Simple one-line scheduling Excellent More verbose
Portability Excellent systemd only
Catch up missed run Not normally Persistent=true
Integrated logs Limited/config-dependent journalctl
Dependencies Script handles them Native unit relationships
Timeouts Script/wrapper required Native service options
Resource controls Limited Extensive
Fleet staggering Manual Randomisation options
Relative-to-boot scheduling Awkward Native
Service status Limited systemctl
Security sandboxing External/manual Service-unit controls
What About anacron?
anacron exists specifically to help with periodic jobs on systems that may not be continuously running.
Unlike ordinary cron, it can catch up certain daily/weekly/monthly tasks after downtime.
It remains useful.
But on a modern systemd-based server, I would generally prefer a persistent systemd timer for new operational jobs because:
scheduling
execution
logging
dependencies
failure state
are all managed within the same framework.
There is no need to rewrite perfectly functional existing anacron jobs purely because systemd exists.
Don't Migrate Working Cron Jobs Just for Fashion
This is important.
If you have a simple cron job that:
has run reliably for five years
produces useful logs
has predictable failure behaviour
is well understood
you do not need to convert it to systemd just because timers are newer.
Migration has value when you gain something.
For example:
persistent catch-up
better logging
dependency management
timeouts
security restrictions
central status
fleet distribution
If none of those matter, leave the working cron job alone.
Do Migrate Important Jobs When Cron Is Becoming a Wrapper Around Everything
A different situation is:
cron
↓
wrapper script
↓
check mount
↓
check network
↓
create lock
↓
run command
↓
capture logs
↓
handle timeout
↓
notify on failure
At that point, you are rebuilding features a service manager already provides.
A systemd service and timer may make the behaviour clearer and easier to maintain.
Test the Command Manually First
Whether using cron or systemd:
prove the command works before scheduling it.
For cron:
/usr/local/bin/backup.sh
For systemd:
sudo systemctl start server-backup.service
Then inspect:
systemctl status server-backup.service
journalctl -u server-backup.service
Do not debug:
the script + scheduler + permissions + environment
all at the same time if you can avoid it.
Use the Correct User
Cron jobs run as whichever user owns the crontab unless configured otherwise.
Systemd services can specify:
User=backup
Group=backup
Do not run every scheduled job as:
root
simply because that makes permission errors disappear.
Use the minimum privileges required.
That limits the impact of:
script bugs
compromised commands
accidental deletions
Absolute Paths Still Matter
Even with systemd, be explicit.
For example:
ExecStart=/usr/local/bin/backup.sh
WorkingDirectory=/srv/application
Do not rely on an interactive shell environment that does not exist when the scheduler executes the job.
The same principle applies to cron.
Scheduled automation should have as few hidden assumptions as possible.
Backups Need More Than “The Timer Ran”
This deserves particular attention.
A timer being green does not prove:
the backup is usable.
Monitor:
exit status
destination capacity
backup logs
retention
restore tests
The same principle applies to cron.
Scheduling proves:
an attempt happened.
It does not prove the business outcome succeeded.
The Decision Rule I Would Use
Choose cron when the job is:
simple + portable + low-risk + okay to miss once.
Choose a systemd timer when the job is:
important + stateful + failure-sensitive + dependent on other system resources.
Examples:
Cron
Clear a temporary directory every Sunday.
Generate a non-critical report.
Run a small personal script.
systemd timer
Business backup.
Certificate maintenance.
Database export.
Server health job.
Anything where you need to know:
Did it run? Did it succeed? What did it log? What should happen if it missed its slot?
Example: Simple Cron Job
30 3 * * * /usr/local/bin/cleanup.sh >> /var/log/cleanup.log 2>&1
Good enough if:
machine is always running
missing one night is fine
script handles its own errors
There is no need to make this more complicated.
Example: Better systemd Backup Timer
Service:
[Unit]
Description=Nightly business backup
RequiresMountsFor=/mnt/backup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/server-backup.sh
TimeoutStartSec=2h
Timer:
[Unit]
Description=Nightly business backup schedule
[Timer]
OnCalendar=02:30
Persistent=true
RandomizedDelaySec=10min
[Install]
WantedBy=timers.target
Then:
sudo systemctl daemon-reload
sudo systemctl enable --now server-backup.timer
Check it:
systemctl list-timers server-backup.timer
and review results:
journalctl -u server-backup.service
That is more configuration than cron.
But it gives you a much stronger operational model.
How Hamilton Group Can Help
Hamilton Group can help businesses manage Linux scheduled workloads including:
cron
systemd timers
Linux backups
automated maintenance
service management
logging
server monitoring
Linux troubleshooting
infrastructure automation
The important part is not whether a scheduled job uses the oldest or newest scheduler.
It is whether the job:
runs reliably, fails visibly and can be understood by the next engineer who has to support it.
Visit hgmssp.com or call 0330 043 0069.