How to Schedule Recurring Jobs With systemd Timers Instead of Cron (September 2026)?

Cron has been the default scheduler on Linux for decades, and it still works fine for simple tasks. But when I started running more complex jobs on production servers, cron’s limitations became obvious. After migrating a dozen jobs from cron to systemd timers on our team’s infrastructure, the difference was clear. Better logging, dependency handling, and sub-minute precision became available without extra tooling.

This guide walks through how to schedule recurring jobs with systemd timers instead of cron, with practical examples you can apply on any modern Linux distribution. By the end, you will know how to write unit files, manage timers, and convert your existing cron jobs.

Why Use systemd Timers Instead of Cron?

Systemd timers replace cron by leveraging the same systemd init system that already manages services on modern Linux. The shift from cron to systemd timers brings several real advantages that I have seen pay off on production systems.

The first major difference is precision and flexibility. Cron only supports one-minute granularity. Systemd timers can trigger jobs in milliseconds using AccuracySec, and they support both calendar-based and monotonic scheduling. This matters when running high-frequency health checks or syncing data between services.

The second difference is integration. Cron jobs run in a near-empty environment with no knowledge of the rest of the system. Systemd timers run as proper units, so you get dependency ordering, automatic restart on failure, resource limits with cgroups, and access to the full systemd ecosystem. When a backup job depends on a database service being up, you can declare that in the unit file.

Logging is where systemd timers truly shine. Cron sends output to local mail or /var/log/syslog with no structure. Systemd timers write directly to the journal, and you can query logs with journalctl -u your-service, filter by time, export to JSON, and forward to centralized logging. When something fails at 3 AM, you will not be reading raw mail files.

Understanding systemd Service and Timer Units

A systemd timer is actually two unit files working together. One file describes what to run, and the other describes when to run it. This separation makes both files simpler and reusable.

The service unit file has a .service extension and defines the command, working directory, user, and any dependencies. It looks almost identical to a regular systemd service. The timer unit file has a .timer extension and defines the schedule using directives like OnCalendar or OnBootSec.

Both files must share the same base name. If you create backup.service, the timer file must be backup.timer. Systemd pairs them automatically. Place system-wide units in /etc/systemd/system/ and user-level units in ~/.config/systemd/user/.

Creating Your First systemd Timer: Step by Step

Let us build a working example. Imagine you want to run a backup script every 15 minutes. We will create both files from scratch and verify the timer runs.

Step 1: Create the service unit file

The service file describes the work. Open a terminal and create /etc/systemd/system/backup.service with sudo:

[Unit]
Description=Nightly backup job
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
WorkingDirectory=/var/backups

The Type=oneshot directive tells systemd this is a short-lived task, not a daemon. The Wants= and After= lines ensure the network is available before the script runs. User=backup makes the script run as a dedicated user rather than root.

Step 2: Create the timer unit file

Now create the matching /etc/systemd/system/backup.timer file:

[Unit]
Description=Run backup every 15 minutes

[Timer]
OnCalendar=*:0/15
Persistent=true
Unit=backup.service

[Install]
WantedBy=timers.target

The OnCalendar=*:0/15 expression means every 15 minutes past the hour. Persistent=true tells systemd to run missed jobs after a reboot. WantedBy=timers.target activates the timer when the timers target is reached during boot.

Step 3: Reload systemd and validate

After creating or modifying unit files, systemd needs to reload them:

sudo systemctl daemon-reload

You can validate the timer schedule before enabling it with the analyze tool:

systemd-analyze calendar "*:0/15"

This command prints the next several trigger times so you can confirm the expression matches what you intended. Catching mistakes here saves hours of debugging later.

Step 4: Enable and start the timer

Finally, enable the timer to start on boot and start it now:

sudo systemctl enable backup.timer
sudo systemctl start backup.timer

Confirm the timer is active with systemctl list-timers --all. You should see backup.timer listed with its next trigger time.

OnCalendar Syntax Explained With Practical Examples

The OnCalendar directive accepts a calendar event specification. The format follows the pattern DayOfWeek Year-Month-Day Hour:Minute:Second, but most fields are optional. Understanding this syntax unlocks precise scheduling.

Here are the most useful expressions I have collected over time:

  • OnCalendar=*:0/15 – Every 15 minutes

  • OnCalendar=hourly – At the top of every hour

  • OnCalendar=daily – At midnight every day

  • OnCalendar=weekly – At midnight on Monday

  • OnCalendar=monthly – At midnight on the first of each month

  • OnCalendar=Mon..Fri 09:00 – Weekdays at 9 AM

  • OnCalendar=2026-01-01 00:00:00 – A specific date and time

  • OnCalendar=Sun 02:00 – Sundays at 2 AM (great for backups)

The slash syntax is the most powerful part. 0/15 in the minute field means every 15 minutes starting at minute 0. You can also use commas for lists and .. for ranges. Mon,Wed,Fri 08:00 runs at 8 AM on Mondays, Wednesdays, and Fridays.

Monotonic Timers: OnBootSec, OnStartupSec, and OnUnitActiveSec

Calendar timers trigger based on wall-clock time. Monotonic timers trigger based on events, which is useful when timing relative to boot or to the last activation of the service.

Three directives cover most use cases. OnBootSec= triggers a fixed time after the system boots. OnStartupSec= triggers a fixed time after the systemd manager itself starts. OnUnitActiveSec= triggers a fixed time after the service unit was last activated.

These are useful for cleanup jobs that should run shortly after boot, or for self-scheduling services that need to run again after finishing. A classic example is a nightly maintenance job that should run 5 minutes after boot if the machine was off during the scheduled window:

[Timer]
OnCalendar=daily
Persistent=true
OnBootSec=5min

Validating Timer Schedules With systemd-analyze calendar

One of the best tools in the systemd toolbox is systemd-analyze calendar. It parses an OnCalendar expression and shows when the timer will actually fire, including after a hypothetical boot.

Run it on any expression to see the next 20 trigger times:

systemd-analyze calendar "Mon..Fri 09:00" --iterations=20

Add the --base= flag with a timestamp to check what would have happened at a specific point in the past. This is invaluable for verifying that a converted cron job will trigger at the same times.

Managing Active Timers: systemctl Commands You Need

Once timers are running, you need to inspect and manage them. These are the commands I use weekly.

List all loaded timers with their next trigger time:

systemctl list-timers

Check the status of a specific timer and its associated service:

systemctl status backup.timer
systemctl status backup.service

View logs from the last run. The -u flag filters by unit:

journalctl -u backup.service -n 50

Stop a timer without disabling it on next boot, or remove it entirely:

sudo systemctl stop backup.timer
sudo systemctl disable backup.timer

If a service fails, use journalctl -u backup.service --since "1 hour ago" to see recent errors. The structured journal output makes it easy to spot patterns.

Cron vs systemd Timers: Feature Comparison

Here is a side-by-side comparison of features that matter when choosing between cron and systemd timers.

  • Granularity: Cron offers 1 minute minimum, systemd timers offer milliseconds with AccuracySec.

  • Dependencies: Cron runs with a stripped environment, while systemd timers integrate with unit dependencies.

  • Logging: Cron relies on local mail, while systemd timers integrate with the journal.

  • Per-user jobs: Cron uses crontab per user, while systemd timers need linger for persistent user timers.

  • Environment: Cron provides a minimal environment, while systemd timers inherit a controlled environment.

  • Missed runs: Cron misses them silently, while systemd timers can catch up with Persistent=true.

  • Resource control: Cron has none, while systemd timers support cgroups and memory limits.

For simple one-off scripts on a desktop, cron remains fine. For anything on a server that needs reliability, logging, or coordination with other services, systemd timers are the better choice.

Per-User Timers and RandomizedDelaySec

Systemd timers can run as a specific user without root. Place unit files in ~/.config/systemd/user/ and enable them with systemctl --user. By default, user timers stop when the user logs out. To keep them running, enable lingering:

sudo loginctl enable-linger username

This is ideal for desktop users who want automated tasks without root access. I have set up per-user timers for syncing notes, cleaning download folders, and rotating personal backups.

When many machines run the same timer at the same time, you get a thundering herd effect. RandomizedDelaySec= adds jitter to spread the load:

[Timer]
OnCalendar=daily
RandomizedDelaySec=30min

This example runs the daily job sometime within a 30-minute window. For fleets of machines running synchronized updates, this single directive can prevent significant load spikes.

How to Convert Existing Cron Jobs to systemd Timers?

Migrating from cron to systemd timers is straightforward once you understand the mapping. Start by listing your current crontab:

crontab -l
ls /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/

For each entry, identify the schedule and command. Translate the cron timing to OnCalendar using the table from earlier. If you have 0 2 * * * /usr/local/bin/backup.sh, the equivalent timer uses OnCalendar=*-*-* 02:00:00.

Create matching service and timer files in /etc/systemd/system/. Test each one with systemd-analyze calendar to confirm the timing matches. Run the service manually first with systemctl start backup.service to verify the command works.

Once the new timer is verified, disable the cron entry by commenting it out or removing it. Keep the old crontab entries for one week as a safety net before final cleanup. This gradual migration approach prevents surprises.

Frequently Asked Questions

What is the difference between cron and systemd timers?

Cron is a standalone scheduler that triggers jobs based on a five-field crontab expression with one-minute minimum granularity. Systemd timers are unit files managed by the init system that support sub-minute precision, dependency ordering, structured logging via the journal, and both calendar-based and event-based triggers. Systemd timers offer better integration with the rest of the system but require writing unit files.

How do I create a systemd timer unit?

Create two unit files with matching names in /etc/systemd/system/. The .service file defines the command to run using ExecStart. The .timer file defines the schedule using OnCalendar or monotonic directives like OnBootSec. Then run systemctl daemon-reload, systemctl enable name.timer, and systemctl start name.timer to activate it.

How do systemd timers work with OnCalendar?

OnCalendar accepts calendar event specifications in the format DayOfWeek Year-Month-Day Hour:Minute:Second, with most fields optional. You can use * for any value, 0/15 for every 15 minutes, Mon..Fri for weekdays, and commas for lists. systemd-analyze calendar expression validates the schedule and shows when it will trigger next.

Can systemd timers run on boot?

Yes. Systemd timers can run on boot using OnBootSec=Nsec, which triggers the service a specified number of seconds after the system boots. You can also combine OnBootSec with OnCalendar to catch up on missed daily jobs after extended downtime. The Persistent=true directive ensures missed runs are executed once the system is back up.

How do I list and manage systemd timers?

Use systemctl list-timers to see all active timers with their next trigger times and units. Manage individual timers with systemctl start, stop, enable, and disable commands. View execution logs with journalctl -u service-name. Test schedules before deployment with systemd-analyze calendar expression.

Wrapping Up: systemd Timers as a Cron Replacement

Systemd timers offer clear improvements over cron for most modern Linux workloads. Sub-minute precision, structured logging, dependency management, and per-user timers all come built in. The migration cost is small once you understand the two-file pattern of service and timer units.

Start with one non-critical job. Write the .service and .timer files, validate with systemd-analyze calendar, and run it for a week. Once you are comfortable, migrate the rest of your crontab. For deeper details, refer to the systemd.timer and systemd.time man pages, which document every directive covered here and more.

Leave a Comment