Skip to main content

Cron vs systemd Timers: Which Should You Use for Scheduled Linux Jobs?

Media Cron vs. Systemd Timers for Scheduled 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.