Cron vs. systemd Timers for Scheduled Jobs
Scheduled jobs quietly keep Linux systems running. They rotate reports, remove temporary files, create backups, renew certificates, synchronise data and perform routine maintenance without waiting for an administrator to start them manually.
For decades, cron has been the standard Linux and Unix scheduling tool. It remains simple, widely understood and perfectly suitable for many tasks.
On modern Linux distributions, however, systemd timers provide another option. They integrate scheduled work with systemd services, logging, dependencies, resource controls and missed-run handling.
Both approaches can run a command at a particular time, but they are not identical. Choosing the right one depends on how important the task is, what should happen after downtime and how much control or visibility you need.
What Is Cron?
Cron is a long-established service that checks scheduling tables called crontabs and starts commands when their configured date and time match.
A typical cron entry looks like this:
30 2 * * * /usr/local/bin/backup.sh
This runs the script every day at 2:30am.
Cron’s main strengths are its simplicity and availability. It is supported across a wide range of Unix-like systems and is familiar to most Linux administrators.
A crontab can contain scheduled commands as well as environment-variable assignments. Each scheduled entry normally contains five time fields followed by the command to run.
What Is a systemd Timer?
A systemd timer is a unit that activates another systemd unit, usually a .service unit, according to a calendar schedule or a period of elapsed time.
Instead of placing the entire job into one scheduling line, you generally create two files:
example-job.service
example-job.timer
The service describes what should run. The timer describes when it should run.
A timer can be based on:
- A calendar date or time
- Time since the machine booted
- Time since the timer was activated
- Time since the service last ran
- Time since the service became inactive
systemd timers support both real-time calendar scheduling and monotonic scheduling based on elapsed time.
The Main Difference
Cron combines the schedule and command in one crontab entry:
0 3 * * * /usr/local/bin/report.sh
A systemd timer separates the job from the schedule.
The service:
[Unit]
Description=Generate the daily report
[Service]
Type=oneshot
ExecStart=/usr/local/bin/report.sh
The timer:
[Unit]
Description=Run the daily report
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
This separation requires more configuration, but it gives systemd much greater control over execution, logging, dependencies and failure handling.
Part One: Scheduling Jobs With Cron
Check Whether Cron Is Running
The service name varies between distributions.
On Ubuntu and Debian:
sudo systemctl status cron
On Fedora, Rocky Linux, AlmaLinux and some other distributions:
sudo systemctl status crond
Start and enable the appropriate service if necessary:
sudo systemctl enable --now cron
or:
sudo systemctl enable --now crond
Edit Your User Crontab
Open the current user’s crontab with:
crontab -e
List its existing entries:
crontab -l
Remove the user’s entire crontab:
crontab -r
Be careful with crontab -r, because it may remove every scheduled job for that user without opening an editor.
To edit root’s crontab:
sudo crontab -e
A job placed in root’s crontab runs with root privileges. Use that only when the command genuinely requires administrative access.
Understanding Cron’s Five Time Fields
A standard user crontab entry uses:
minute hour day-of-month month day-of-week command
For example:
15 4 * * * /usr/local/bin/cleanup.sh
This means:
- Minute: 15
- Hour: 4
- Every day of the month
- Every month
- Every day of the week
The job runs daily at 4:15am.
Common Cron Examples
Run every hour:
0 * * * * /usr/local/bin/hourly-task.sh
Run every day at midnight:
0 0 * * * /usr/local/bin/daily-task.sh
Run every Monday at 8:30am:
30 8 * * 1 /usr/local/bin/weekly-report.sh
Run on the first day of every month:
0 2 1 * * /usr/local/bin/monthly-task.sh
Run every 15 minutes:
*/15 * * * * /usr/local/bin/check-service.sh
Run Monday to Friday at 6:00pm:
0 18 * * 1-5 /usr/local/bin/business-day-task.sh
Cron Shortcut Expressions
Many cron implementations support shortcuts such as:
@reboot
@hourly
@daily
@weekly
@monthly
@yearly
For example:
@reboot /usr/local/bin/startup-task.sh
This runs when the cron service starts after boot.
A daily job could be written as:
@daily /usr/local/bin/daily-task.sh
Shortcut support can differ between cron implementations, so confirm what is available on the target system.
Use Absolute Paths
Cron runs with a limited environment and may not use the same PATH as your interactive shell.
This job may fail:
0 2 * * * backup-tool --run
A safer version uses the full executable path:
0 2 * * * /usr/local/bin/backup-tool --run
You can find a command’s path with:
command -v backup-tool
Also use full paths for files and directories inside scripts.
Instead of:
cd reports
use:
cd /srv/application/reports
Define the Cron Environment Explicitly
You can define environment variables at the top of a crontab:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
30 2 * * * /usr/local/bin/backup.sh
Cron does not automatically reproduce your full interactive login environment. Commands that work in a terminal may fail under cron because aliases, shell profiles, language runtimes or application variables are missing.
A script should set or load everything it requires.
Redirect Cron Output
Cron may email command output to the owner of the crontab, depending on how mail delivery is configured.
To send output to a log file:
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
This appends both standard output and standard error to the same file.
The components mean:
>> append standard output
2>&1 send standard error to the same destination
Make sure the user running the job can write to the chosen log path.
A user-level job might log to:
30 2 * * * /home/exampleuser/logs/backup.log 2>&1
Avoid discarding output while you are still testing.
This hides all output:
30 2 * * * /usr/local/bin/backup.sh >/dev/null 2>&1
That may keep things quiet, but it can also conceal failures.
Test the Command Outside Cron First
Before scheduling a script, run it as the same user that cron will use:
sudo -u exampleuser /usr/local/bin/backup.sh
Check its exit status:
echo $?
An exit status of 0 usually indicates success.
Confirm that the script:
- Uses absolute paths
- Has execute permission
- Does not require interactive input
- Can access its files
- Can write its output
- Has all required environment variables
- Handles errors correctly
Make the script executable:
sudo chmod +x /usr/local/bin/backup.sh
Check Cron Logs
Depending on the distribution, cron activity may appear in the system journal:
sudo journalctl -u cron
or:
sudo journalctl -u crond
Follow the log live:
sudo journalctl -fu cron
Some distributions also write cron entries to files such as:
/var/log/syslog
/var/log/cron
Search the journal for a specific command:
sudo journalctl | grep backup.sh
Cron logs may confirm that the scheduler launched the command, but they do not always capture the command’s own output unless you redirect it.
System Crontabs
In addition to per-user crontabs, Linux systems may use:
/etc/crontab
/etc/cron.d/
/etc/cron.hourly/
/etc/cron.daily/
/etc/cron.weekly/
/etc/cron.monthly/
The format of /etc/crontab and files under /etc/cron.d normally includes an extra username field:
minute hour day month weekday user command
Example:
0 3 * * * root /usr/local/bin/system-backup.sh
Do not include that username field in a normal user crontab created with crontab -e.
Cron and Missed Jobs
Traditional cron normally runs a job only when its scheduled time occurs while the machine and cron daemon are running.
Suppose a laptop has this entry:
0 2 * * * /usr/local/bin/backup.sh
If the laptop is switched off at 2:00am, the job will usually not run when it is next powered on. It simply waits for the following scheduled occurrence.
Tools such as Anacron can help with periodic jobs that should run after missed execution windows, but standard cron alone does not provide systemd timer-style persistent catch-up behaviour.
Part Two: Scheduling Jobs With systemd Timers
Create the Service Unit
Suppose you want to run:
/usr/local/bin/backup.sh
Create a service unit:
sudo nano /etc/systemd/system/server-backup.service
Add:
[Unit]
Description=Create the server backup
Documentation=man:systemd.service(5)
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Type=oneshot is suitable for a command that runs, completes and exits.
The service does not need an [Install] section when it will be activated exclusively by the timer.
Create the Timer Unit
Create:
sudo nano /etc/systemd/system/server-backup.timer
Add:
[Unit]
Description=Run the server backup every night
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
Unit=server-backup.service
[Install]
WantedBy=timers.target
The Unit= line identifies the service to activate.
When the timer and service share the same base name, systemd can infer the service automatically, so this would also work without the explicit Unit= line:
server-backup.timer
server-backup.service
Including it can still make the relationship clear.
Enable and Start the Timer
Reload systemd after creating or modifying unit files:
sudo systemctl daemon-reload
Enable the timer at boot and start it now:
sudo systemctl enable --now server-backup.timer
Check its status:
systemctl status server-backup.timer
List all timers:
systemctl list-timers
Include inactive timers:
systemctl list-timers --all
The output normally shows:
- The next scheduled activation
- The time remaining
- The previous activation
- The timer unit
- The service it triggers
Test the Service Before Waiting for the Timer
Run the service manually:
sudo systemctl start server-backup.service
Check its result:
systemctl status server-backup.service
Inspect the logs:
sudo journalctl -u server-backup.service
Show logs from the current boot:
sudo journalctl -u server-backup.service -b
Follow them live:
sudo journalctl -fu server-backup.service
This is one of the major advantages of systemd timers: the scheduled task behaves like any other managed service and logs directly to the journal by default.
Validate Unit Files
Check unit-file syntax with:
sudo systemd-analyze verify \
/etc/systemd/system/server-backup.service \
/etc/systemd/system/server-backup.timer
This can identify unknown directives, dependency problems and some formatting mistakes.
After every edit:
sudo systemctl daemon-reload
Then restart the timer if needed:
sudo systemctl restart server-backup.timer
Understanding
OnCalendar
OnCalendar= defines real-time schedules.
Every day at 2:30am:
OnCalendar=*-*-* 02:30:00
Every Monday at 8:00am:
OnCalendar=Mon *-*-* 08:00:00
Every hour:
OnCalendar=hourly
Every day:
OnCalendar=daily
Every 15 minutes:
OnCalendar=*:0/15
The systemd calendar syntax supports more expressive schedules than traditional cron in many situations. Calendar expressions used by timer units are described by systemd’s time syntax.
Validate a Calendar Expression
Use:
systemd-analyze calendar 'Mon *-*-* 08:00:00'
This displays the normalised expression and the next scheduled occurrence.
For example:
systemd-analyze calendar '*:0/15'
This is extremely useful when creating or reviewing timer schedules.
Run a Job After Boot
To run a task five minutes after the system starts:
[Timer]
OnBootSec=5min
This is a monotonic timer based on elapsed time since boot.
Run it 30 minutes after the timer becomes active:
OnActiveSec=30min
Run one hour after the service last activated:
OnUnitActiveSec=1h
Run one hour after the service last became inactive:
OnUnitInactiveSec=1h
These elapsed-time options are difficult to express cleanly with traditional cron.
Combine Calendar and Elapsed-Time Triggers
A timer may contain more than one trigger:
[Timer]
OnBootSec=10min
OnUnitActiveSec=1h
This runs the service ten minutes after boot and then approximately every hour.
You can also define multiple OnCalendar= lines:
[Timer]
OnCalendar=Mon *-*-* 08:00:00
OnCalendar=Fri *-*-* 17:00:00
The same service will run at either scheduled time.
Catch Up After Downtime With
Persistent=true
A timer containing:
Persistent=true
records when it last activated.
When the timer becomes active again, systemd checks whether an OnCalendar= event was missed while the timer was inactive. If so, it can trigger the service promptly rather than waiting for the next occurrence. This persistent catch-up behaviour applies to calendar timers.
Example:
[Timer]
OnCalendar=daily
Persistent=true
If the machine was switched off during the scheduled time, the job can run after it next starts.
This makes systemd timers particularly useful for:
- Laptops
- Workstations
- Intermittently running servers
- Maintenance tasks that should not be silently skipped
Be careful with jobs that should not run immediately after boot or after a long outage. A missed backup or cleanup task may be appropriate to catch up, while a time-sensitive notification may not be.
Add a Random Delay
If hundreds of systems all run the same task at exactly midnight, they may overload a server or network.
Add:
RandomizedDelaySec=30min
Example:
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=30min
The timer runs within a random delay window rather than having every host start simultaneously.
This is useful for:
- Update checks
- Repository synchronisation
- Cloud backups
- Monitoring uploads
- Fleet-wide maintenance
RandomizedDelaySec= is specifically designed to distribute timer activations across a time window.
Timer Accuracy
systemd may deliberately coalesce nearby timers to reduce unnecessary wake-ups and improve efficiency.
The default timing accuracy is not always intended to provide second-perfect execution.
You can adjust it with:
AccuracySec=1s
Example:
[Timer]
OnCalendar=*-*-* 02:30:00
AccuracySec=1s
Do not request extremely precise scheduling unless the job genuinely requires it. Most backups, reports and maintenance tasks do not need exact-to-the-second execution.
AccuracySec= controls the window in which a timer may be scheduled, while RandomizedDelaySec= adds an intentional random delay. They serve different purposes.
Prevent Overlapping Runs
Imagine a timer starts every ten minutes, but the job occasionally takes 20 minutes.
With a normal timer-linked systemd service, attempting to start an already active service does not create a second simultaneous instance of that same service unit.
That provides useful protection against accidental overlap.
For cron, you normally need to add your own locking.
A cron example using flock:
*/10 * * * * /usr/bin/flock -n /run/report-job.lock /usr/local/bin/report.sh
The -n option causes the new run to exit if the lock is already held.
For any scheduler, decide what should happen when the previous job is still running:
- Skip the new run
- Wait for the lock
- Terminate the old run
- Alert an administrator
- Allow parallel execution
Do not leave this behaviour to chance.
Add a Runtime Limit
A systemd service can stop a job that runs for too long:
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
TimeoutStartSec=2h
If the job does not complete within two hours, systemd treats it as failed and stops it.
Cron does not provide an equivalent service-level timeout itself, although you can use the timeout command:
30 2 * * * /usr/bin/timeout 2h /usr/local/bin/backup.sh
Set the Working Directory
A systemd service can define its working directory explicitly:
[Service]
Type=oneshot
WorkingDirectory=/srv/application
ExecStart=/usr/local/bin/report.sh
This is clearer and safer than assuming the script will start from a particular directory.
Cron jobs should normally change directory explicitly:
0 3 * * * cd /srv/application && /usr/local/bin/report.sh
Set Environment Variables
A systemd service can define variables directly:
[Service]
Type=oneshot
Environment="REPORT_MODE=daily"
Environment="OUTPUT_DIR=/srv/reports"
ExecStart=/usr/local/bin/report.sh
Or load them from a file:
EnvironmentFile=/etc/default/report-job
Example environment file:
REPORT_MODE=daily
OUTPUT_DIR=/srv/reports
Protect files containing credentials:
sudo chown root:root /etc/default/report-job
sudo chmod 600 /etc/default/report-job
Do not place secrets directly in publicly readable unit files or crontabs.
Run the Service as a Dedicated User
System-level systemd services run as root by default.
Reduce privileges with:
[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/bin/backup.sh
The specified account must have access to every required file and destination.
The cron equivalent is to place the job in that user’s crontab:
sudo crontab -u backup -e
Or use the username field in /etc/cron.d.
Add Service Dependencies
Suppose a job needs the network:
[Unit]
Description=Upload the daily report
Wants=network-online.target
After=network-online.target
A job that depends on a mounted filesystem could use:
[Unit]
RequiresMountsFor=/srv/backups
This tells systemd that the path must be available for the service.
Dependencies are one of the strongest reasons to choose a systemd timer over cron. Cron merely starts a command at a time; it does not inherently understand service ordering, mount requirements or network targets.
Remember that network-online.target does not guarantee that a particular remote server or internet service is reachable. The script still needs proper retries and error handling.
Add Security Restrictions
systemd services can apply sandboxing and hardening options:
[Service]
Type=oneshot
User=reporter
ExecStart=/usr/local/bin/generate-report.sh
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/reports
These settings can reduce the damage caused by a compromised or faulty script.
Test hardening options carefully because they may block legitimate access.
Cron itself does not provide equivalent per-job sandboxing. You would need to rely on user permissions, containers or separate security tools.
Configure Failure Notifications
A systemd service can trigger another unit when it fails:
[Unit]
Description=Create the server backup
OnFailure=backup-failure-notification.service
The notification service might send an email, create a monitoring alert or call an internal webhook.
You can also monitor failed units:
systemctl --failed
Check the result of the latest service run:
systemctl status server-backup.service
Cron can also send output by email or invoke alerting commands, but failure handling must generally be built into the command or wrapper script.
User-Level systemd Timers
Timers do not always need to be installed system-wide.
A user can place units under:
~/.config/systemd/user/
For example:
~/.config/systemd/user/report.service
~/.config/systemd/user/report.timer
Reload the user manager:
systemctl --user daemon-reload
Enable the timer:
systemctl --user enable --now report.timer
List user timers:
systemctl --user list-timers
View user-service logs:
journalctl --user -u report.service
By default, user services may stop when the user logs out. To allow the user manager to remain active without an interactive login, an administrator can enable lingering:
sudo loginctl enable-linger username
Consider the security and resource implications before enabling lingering broadly.
Cron and systemd Timer Comparison
Configuration
Cron normally uses one compact line:
30 2 * * * /usr/local/bin/backup.sh
systemd normally uses a service unit and a timer unit.
Cron wins for brevity. systemd wins when the job needs structured configuration.
Portability
Cron is available across Linux, BSD and many other Unix-like systems.
systemd timers require a system using systemd.
For scripts that must run across varied Unix platforms, cron is usually more portable.
Logging
Cron often requires explicit output redirection or a functioning local mail system.
systemd automatically associates service output with the journal:
journalctl -u example.service
systemd normally provides better centralised visibility.
Missed Runs
Traditional cron normally skips a job if the machine is off at its scheduled time.
A systemd calendar timer with:
Persistent=true
can run after the machine returns.
Dependencies
Cron starts the command based on time.
systemd can express relationships involving:
- Network readiness
- Mounted filesystems
- Other services
- Boot targets
- Ordering
- Required units
Resource and Security Controls
systemd services can apply:
- CPU limits
- Memory limits
- I/O controls
- Dedicated users
- Filesystem restrictions
- Private temporary directories
- Capability restrictions
- Runtime timeouts
Cron relies mainly on the user account and whatever controls the command implements.
Testing
A cron command should be tested manually, and some cron implementations offer syntax checking.
With systemd, you can test the service directly:
sudo systemctl start example.service
and validate units with:
systemd-analyze verify example.service example.timer
Job Overlap
Cron may launch another copy unless you implement locking.
A timer usually activates a named service unit, which naturally prevents another simultaneous activation while that unit remains active.
Complexity
Cron is easier to understand for a simple job.
systemd timers involve more files and concepts, but they scale better when the task becomes operationally important.
When Cron Is the Better Choice
Cron remains an excellent choice when:
- The job is simple.
- The system is always running.
- Missing one occurrence is acceptable.
- Portability matters.
- The command has no complicated dependencies.
- The existing environment already manages cron reliably.
- The administrator needs a small personal scheduled task.
- A one-line schedule is easier to maintain than two unit files.
For example:
0 6 * * 1-5 /home/user/bin/create-personal-report.sh
There may be little benefit in replacing this with a systemd timer if it is already reliable and well monitored.
When a systemd Timer Is the Better Choice
Use a systemd timer when:
- The task must catch up after downtime.
- You need detailed journal logging.
- The task depends on a mount, network or service.
- You need execution timeouts.
- The job should run as a restricted user.
- You want resource or filesystem controls.
- You need structured failure handling.
- The job must not overlap.
- You are managing a modern systemd-based server.
- The scheduled task is operationally important.
Backups, database maintenance, synchronisation services and infrastructure jobs often benefit from systemd’s additional controls.
Converting a Cron Job to a systemd Timer
Suppose the existing cron entry is:
15 1 * * * /usr/local/bin/archive-logs.sh
Create the Service
sudo nano /etc/systemd/system/archive-logs.service
Add:
[Unit]
Description=Archive application logs
[Service]
Type=oneshot
ExecStart=/usr/local/bin/archive-logs.sh
User=root
Create the Timer
sudo nano /etc/systemd/system/archive-logs.timer
Add:
[Unit]
Description=Archive application logs every night
[Timer]
OnCalendar=*-*-* 01:15:00
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
Validate and Test
sudo systemctl daemon-reload
sudo systemd-analyze verify \
/etc/systemd/system/archive-logs.service \
/etc/systemd/system/archive-logs.timer
Run the service manually:
sudo systemctl start archive-logs.service
Review the logs:
sudo journalctl -u archive-logs.service
Validate the schedule:
systemd-analyze calendar '*-*-* 01:15:00'
Enable the Timer
sudo systemctl enable --now archive-logs.timer
Confirm it:
systemctl list-timers archive-logs.timer
Remove the Cron Entry Last
Only after the timer has been tested should you remove the original cron entry.
Otherwise, both schedulers may run the same job.
This can result in:
- Duplicate backups
- Duplicate emails
- Conflicting maintenance
- Excessive load
- Corrupted output
- Overlapping processes
Common Cron Problems
The Command Works Manually but Not in Cron
Likely causes include:
- A missing PATH
- Relative paths
- Missing environment variables
- Different shell behaviour
- Insufficient permissions
- A required interactive terminal
- A missing working directory
Test the command with a minimal environment:
env -i \
PATH=/usr/bin:/bin \
HOME=/home/exampleuser \
/usr/local/bin/example-script.sh
Cron Runs the Wrong Shell
Cron commonly uses /bin/sh unless another shell is configured.
A script that requires Bash should begin with:
#!/usr/bin/env bash
and be executed directly:
0 2 * * * /usr/local/bin/example-script.sh
Alternatively, define:
SHELL=/bin/bash
at the top of the crontab.
Cron Has No Output
Redirect output while testing:
* * * * * /usr/local/bin/test-job.sh >> /tmp/test-job.log 2>&1
Also check the cron service logs.
The Percent-Sign Problem
In many cron implementations, an unescaped % in the command has special meaning and may be converted into a newline.
Escape a literal percent sign:
0 1 * * * /bin/date +\%F >> /tmp/date.log
A safer option is to place complicated commands inside a script rather than embedding them directly in the crontab.
Day-of-Month and Day-of-Week Confusion
Cron’s handling of the day-of-month and day-of-week fields can surprise administrators.
When both fields are restricted rather than set to *, traditional cron implementations may run the job when either field matches rather than only when both match.
Avoid ambiguous expressions. Use a wrapper script that checks the date when the requirement is complicated.
Common systemd Timer Problems
The Timer Is Active but the Service Never Runs
Check the next activation:
systemctl list-timers --all
Inspect the timer:
systemctl status example.timer
Validate the calendar:
systemd-analyze calendar 'your expression'
Confirm that the service name matches the timer or that Unit= points to the correct service.
The Service Failed Immediately
Check:
systemctl status example.service
sudo journalctl -u example.service
Common causes include:
- The executable path is wrong.
- The script lacks execute permission.
- The configured user cannot access the files.
- An environment variable is missing.
- A security restriction blocks the command.
- The working directory does not exist.
Edits Do Not Take Effect
After changing a unit file:
sudo systemctl daemon-reload
Restart the timer:
sudo systemctl restart example.timer
If you changed the service, test it manually again.
The Job Ran Immediately After Boot
This may be expected when:
Persistent=true
is enabled and a calendar event was missed during downtime.
Remove Persistent=true when catch-up behaviour is inappropriate.
The Timer Appears Late
Review:
AccuracySec=
RandomizedDelaySec=
A timer may activate within an accuracy window or after an intentional random delay.
Also check the system clock and timezone:
timedatectl
The Service Remains “Active” and Never Runs Again
A oneshot service that uses:
RemainAfterExit=yes
may remain active after the command finishes. A timer cannot meaningfully reactivate an already active unit.
For most scheduled scripts, use:
Type=oneshot
without RemainAfterExit=yes.
Security Considerations
Scheduled jobs often run unattended and may have elevated access.
Apply the following principles whichever scheduler you use:
- Run the task with the least privilege required.
- Use scripts owned by an appropriate administrator.
- Prevent untrusted users from modifying scheduled executables.
- Protect environment and credentials files.
- Use absolute command paths.
- Validate arguments and input files.
- Avoid writing to insecure temporary paths.
- Log failures.
- Apply timeouts.
- Prevent overlapping runs.
- Review old jobs regularly.
- Remove schedules when an application is retired.
Check script ownership:
ls -l /usr/local/bin/backup.sh
A root-run script should not normally be writable by an ordinary user:
sudo chown root:root /usr/local/bin/backup.sh
sudo chmod 750 /usr/local/bin/backup.sh
Otherwise, someone who can alter the script may gain the privileges of the scheduled job.
A Practical Decision Guide
Use cron when the requirement is:
Run this straightforward command at this straightforward time.
Use a systemd timer when the requirement is closer to:
Run this managed service according to a schedule, record its output, handle missed runs, respect dependencies, limit its privileges and make failures visible.
Neither scheduler is universally superior.
Cron’s simplicity is a strength, not a weakness. systemd’s additional structure is valuable when a job becomes important enough to need operational controls.
Final Thoughts
Cron remains one of the fastest and most portable ways to schedule a command on Linux. For small jobs on machines that stay powered on, it is often all you need.
systemd timers provide a more integrated approach for modern Linux systems. They offer journal logging, service dependencies, persistent catch-up runs, random delays, security restrictions and better visibility into the job’s latest result.
The right question is not simply, “Which scheduler is newer?”
It is:
- What happens if the machine is off?
- How will failures be detected?
- Can two copies run at once?
- Does the job need the network or a mounted drive?
- Which account should run it?
- Where will its logs appear?
- How easily can another administrator understand it?
For a simple personal task, cron may remain the clearest solution. For an important server process, a systemd timer will often provide the control and auditability needed to manage it properly.
Need Help Automating Linux Maintenance?
Hamilton Group can help configure reliable Linux scheduled jobs, backup routines, reporting tasks, service checks and system maintenance.
We can review existing cron entries, migrate important jobs to systemd timers and make sure failures, missed runs and permissions are handled correctly.
Call 0330 043 0069 or visit hgmssp.com to speak with one of our IT specialists.