Learn Linux - Task Automation Using Cron Jobs & Systemd Timers
Series/Learn Linux/Episode 22
Episode 22 of 31

Learn Linux - Task Automation Using Cron Jobs & Systemd Timers

Automating recurring server tasks with Cron Jobs and Systemd Timers: the five-column cron syntax, redirecting output to log files, to the service + timer unit pairing for a more robust schedule. Complete with a daily backup script scheduling case study and the traps admins often hit.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Introduction

After episode 21 where we covered system monitoring — how to observe resource usage, running processes, and logs to know the server's condition at any time — in this episode we'll shift perspective: from merely seeing problems to preventing problems through automation.

Maintaining a server is like managing an apartment. There are routines that must be done every day: taking out the trash, checking the lights, making sure the doors are locked, turning on the heater before the residents wake up. If all of that is done manually by one person, sooner or later something gets missed — and what gets missed is usually the most important thing. This is where task automation comes in: cron and systemd timers are the "assistants" that never forget, never get sick, and never take a day off. They work in the background at exactly the times you specify, and record what they did.

In the DevOps world, almost all routine operations — database backups, log rotation, TLS certificate renewal, cleaning the tmp directory, and data sync between servers — are run by schedulers like these. Mastering them isn't just learning two commands, but understanding the mental model of scheduling work correctly: when to run it, how to track its results, and what happens if it fails.

In this episode we'll cover cron first as the classic approach that still dominates, then systemd timers as a more robust modern alternative. We'll compare both in a table, practice by scheduling a daily backup script, and close with the common mistakes that most often trap new admins.

Main Discussion

Why Task Automation Is Important

Before discussing the tools, let's understand why automation isn't just convenience, but an operational necessity.

First, consistency. A script run by a scheduler every day produces identical results over time — unlike human hands that can slip. Second, punctuality. A log-cleaning script run at 3.00 AM when traffic is quiet has a different impact than one run manually during peak hours. Third, auditability. The scheduler records when a task ran and what its results were — a trail that's invaluable when you have to answer the question "why was data lost on date X?" later on.

Imagine a real scenario: a production database backs itself up with mysqldump every night. One day the disk fails and the team must restore data from backups. Without automation, the last backup might be a week old because "we forgot yesterday". With a scheduler, the backup always exists, always on time, and is just a restore away. That's the difference between an admin who calmly faces disasters and one who panics.

Understanding Cron: The Basics

Cron is the classic scheduling daemon that has accompanied Linux since the early Unix era. It works by reading a file called crontab (cron table), containing a list of lines that each determine when a command is run.

There are several crontab locations you should know:

LocationUserWhen Executed
crontab -ePer-userThe standard cron command for a specific user
/etc/crontabRoot (system)System-wide, may add a user column
/etc/cron.d/RootSeparate crontab files, often used by packages
/etc/cron.hourly/, /etc/cron.daily/RootScripts inside these folders run on their folder's schedule

To manage your own crontab, there are just three most-used commands:

The three most basic crontab commands
crontab -e   # open the current user's crontab in an editor
crontab -l   # display the current crontab contents
crontab -r   # delete the entire crontab (careful!)

The heart of cron is the five-column syntax. Each crontab line has five time columns followed by the command to run:

Anatomy of the cron syntax: five columns + command
* * * * * /usr/bin/backup.sh
┬ ┬ ┬ ┬ ┬
│ │ │ │ └──── day of week (0-7, 0 & 7 = Sunday)
│ │ │ └────── month (1-12)
│ │ └──────── day of month (1-31)
│ └────────── hour (0-23)
└──────────── minute (0-59)

The * sign means "every value in that column". You can also write comma-separated value lists (1,15,30), ranges (1-5), and intervals (*/15 = every 15). Here are the translations of the most-used patterns:

ExpressionMeaning
* * * * *Every minute
*/15 * * * *Every 15 minutes
0 3 * * *Every day at 3.00 AM
30 5 * * 1Every Monday at 5.30 AM
0 2 1 * *Every 1st of the month at 2.00 AM
0 0 * * 1-5Every workday (Mon–Fri) at midnight

The Cron Execution Environment & Redirecting Output

One of the facts that most often confuses admins: cron doesn't run commands with an environment like your terminal. It doesn't load ~/.bashrc, doesn't have a full PATH, and doesn't inherit environment variables from a login session. Cron only executes commands with a minimal PATH — usually enough for sh, rm, cp, and similar, but not for binaries living in /usr/local/bin/.

The result: writing crontab -e and filling in 0 3 * * * mybackup produces a command not found error in the log — not because the command is wrong, but because cron doesn't know where mybackup is. The solution is always: use absolute paths, or set PATH explicitly at the top of the crontab.

The second problem is output that's never seen. When a cron script prints output, cron collects it and sends it via local email — which on minimalist servers is often not installed (postfix isn't), so the output silently disappears. But that output is the only trace that the script ran. The solution: explicitly direct the output to a log file.

From lost output to logged output
0 3 * * * /usr/local/bin/backup.sh
0 3 * * * /usr/local/bin/backup.sh >> /var/log/mycron.log 2>&1

Let's dissect the marked line. >> is append — adding output to the end of a file without deleting old content (remember the redirection operators from episode 5). 2>&1 merges stderr into stdout, so errors are also recorded in the same file. The order matters: >> /var/log/mycron.log 2>&1 means "stdout goes into the file, then stderr follows the same direction". The result: one log file containing the entire execution trail — easy to grep and no output lost.

Warning

Always write absolute paths for both commands and files inside the crontab. Cron doesn't load the PATH from a login shell, so pg_dump or restic living in /usr/local/bin/ won't be found. Write /usr/local/bin/pg_dump — or, more tidily, define PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin on the first line of your crontab.

Systemd Timers: The Modern Alternative

Cron is decades old and still works — but it has structural limitations that are felt on modern servers. This is where systemd timers come in as a more robust replacement.

What are the advantages? First, integration with journald. Script output is directly recorded in the journal (journalctl), no need to manually redirect output. Second, explicit dependencies. You can ensure a timer only runs after the network is online, or after another service is up. Third, flexible triggers — not only calendar-based (OnCalendar), but also monotonic ones like OnBootSec (15 minutes after boot) or OnUnitActiveSec (every 1 day after the last execution). Fourth, missed-run handling: with Persistent=true, if the server is off when the schedule arrives, the timer will run the task the moment the server comes back — behavior cron doesn't have.

The analogy: cron is like an alarm clock that still rings even when you're not home. A systemd timer is like an assistant who knows the schedule, and when you come back, he reports "something was missed yesterday, I've taken care of it".

Like all systemd units, a timer works in pairs: one .service file defining what is done, and one .timer file defining when it's done. Both go in /etc/systemd/system/:

[Unit]
Description=Daily backup service
After=network-online.target
 
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

Note several important details. Type=oneshot tells systemd this service runs briefly and finishes — not a daemon that keeps running. OnCalendar=*-*-* 03:00:00 is systemd's time expression equivalent to 0 3 * * * in cron. Persistent=true handles the case of a server being off when the schedule arrives. And WantedBy=timers.target binds the timer to the special timer target.

After both files are created, enable and verify:

Enable the timer and check its schedule
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
Example systemctl list-timers output
NEXT                        LEFT          LAST                        PASSED  UNIT            ACTIVATES
Sat 2026-08-03 03:00:00 WIB 15h 19min left Fri 2026-08-02 03:00:00 WIB 8h ago  backup.timer    backup.service
Sat 2026-08-03 06:55:45 WIB 19h left     n/a                         n/a     fstrim.timer    fstrim.service
1 timers listed.
The NEXT column shows the upcoming schedule; LAST shows the last execution

The systemctl list-timers output will show the next schedule (NEXT) and the last execution (LAST). To see the execution trail of the service, just read the journal:

View the service execution results from journald
journalctl -u backup.service -b

Tip

systemd timers excel because the execution trail is automatically recorded in journald with tidy structure and timestamps. For cron, you must set up redirection yourself (>> log 2>&1) and guess if you forget. If your server is already systemd-based — and all modern distros have used it since episode 14 — there's no strong reason to refuse timers.

Cron vs Systemd Timers Comparison

Here's a concise comparison that can guide decisions:

AspectCronSystemd Timers
Time syntax5 columns (0 3 * * *)OnCalendar / OnBootSec / OnUnitActiveSec
Script outputMust redirect manuallyAutomatically in journald
DependenciesNoneDeclared in [Unit]
Missed schedule at shutdownNot caught upCan catch up with Persistent=true
Gap between executionsNone (calendar time only)OnUnitActiveSec — easy
EnvironmentMinimal PATHUnit environment that can be configured
CompatibilityAll systemsOnly systemd-based systems

When to use which? If you just need "run this every midnight" on a non-systemd system (e.g. Alpine, or a minimal container), cron is still perfect. If you manage a modern server and want tidy log trails, clear dependencies, and schedules that don't easily get missed, systemd timers are the better choice.

Practice: Schedule a Daily Backup Script

Time to combine everything. We'll create a simple backup script, then schedule it with both approaches.

First, the backup script. This script takes a PostgreSQL database dump, compresses it, saves it in a dated directory, and cleans up files older than 7 days — so the disk doesn't fill up with old backups.

/usr/local/bin/backup.sh
#!/usr/bin/env bash
set -euo pipefail
 
BACKUP_DIR=/srv/backups/daily
mkdir -p "$BACKUP_DIR"
 
pg_dump -U myapp myapp_db | gzip > "$BACKUP_DIR/myapp-$(date +%F).sql.gz"
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 -delete

Note set -euo pipefail on the second line — this ensures the script stops and returns a failure status if any step errors. Without it, a pg_dump failure could slip through unnoticed because the | gzip pipeline still "succeeds". Make sure the script file is executable: chmod +x /usr/local/bin/backup.sh.

Once the script is ready, schedule it. The cron version — add to crontab -e:

Cron entry for the daily backup
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

The systemd timer version — two files exactly like in the previous section: backup.service runs the script, backup.timer schedules it at 3.00 AM with Persistent=true. Both achieve the same thing, but the systemd version gives you a journal trail and guaranteed execution if the server was briefly down.

Caution

Test the script manually before scheduling it. Run sudo /usr/local/bin/backup.sh once in the terminal and see if the dump forms. A script never run manually that goes straight into cron is an invitation to problems discovered only weeks later — exactly the "backup ran but the data is empty" pattern so common in the real world.

Common Mistakes (Common Pitfalls)

1. Forgetting absolute paths. Already discussed: cron doesn't load the full PATH. pg_dump, restic, or scripts in /usr/local/bin won't be found. Use absolute paths or set PATH at the start of the crontab.

2. Minute column errors. 0 3 * * * means exactly 3.00 AM; * 3 * * * means every minute during hour 3 (60 executions!). The one-character difference between * and 0 is the difference between "once a day" and "60 times per hour". Always verify with crontab -l after writing.

3. Output not redirected. Without >> log 2>&1, cron output is sent to a local email that isn't installed, then lost. Every cron entry on a production server must direct output to a log file.

4. Forgetting daemon-reload after creating a systemd unit. systemd caches already-known units. After writing or changing a .service/.timer file, you must run systemctl daemon-reload before the enable command processes correctly.

5. Not checking whether the timer is actually active. systemctl enable backup.timer without --now only enables it for the next boot — the timer isn't running now. Use enable --now and verify with systemctl list-timers.

Conclusion

In this episode 22, you've mastered the two approaches to Linux task automation. We started with cron: reading and managing the crontab with crontab -e, crontab -l, and crontab -r, understanding the five-column syntax, and redirecting output with >> /var/log/mycron.log 2>&1 so every execution is recorded. Then we moved to systemd timers: the .service + .timer unit pairing, the OnCalendar expression, Persistent=true, and the advantage of journald integration. We compared both in a table, scheduled a daily backup script, and closed with the traps admins most often hit.

Key points to take with you:

  • Cron syntax: minute hour day-of-month month day-of-week — pay attention to the difference between 0 and * in the minute column.
  • Always use absolute paths and redirect output to a log file (>> ... 2>&1).
  • systemd timers are more robust for modern servers: journald, dependencies, and Persistent=true.
  • After writing a systemd unit, don't forget systemctl daemon-reload then enable --now.
  • Test scripts manually before scheduling — don't let the scheduler run something never proven.

With the ability to schedule tasks, you're ready to discuss the raw material of all that automation: logs. In the next episode 23 we'll dismantle System Logging, Rotation & Auditing — understanding the /var/log/ architecture, getting to know important logs like syslog, auth.log, and boot.log, managing rotation with logrotate so the disk doesn't fill up, and an introduction to rsyslog for centralized logging and auditd for security trails. Stay sharp!

Learn Linux - Task Automation Using Cron Jobs & Systemd Timers | Learn Linux