sysadmin · June 19, 2026 · 14 min read

Scheduling work.
cron, at, and systemd timers, and when to use which.

logadmin1 needs a daily backup, a weekly cleanup, and a one-shot update tonight. Three scheduling tools, one for each job, all logging so you know they ran.

// what we’re getting into
  1. The ticket
  2. Concept review. Three tools, three jobs.
  3. cron for the daily backup
  4. at for the one-shot update
  5. A systemd timer for the weekly cleanup
  6. cron time patterns worth reading aloud
  7. Exam questions
  8. Final checklist: confirm everything works

logadmin1 has three jobs that need to run with nobody watching. The /etc backup from the backup-pipeline ticket, every night at 2:30. A sweep of tarballs older than 14 days, every Sunday at 4. And a one-off dnf upgrade tonight at 11, once, never again. Three schedules, and the twist in the ticket is that each one wants a different tool. That is not overkill. It is the right answer to three different shapes of problem.

Recurring and simple is cron. One time in the future is at. Modern, with calendar syntax and the ability to catch up a run it missed while the box was powered off, is a systemd timer. And each of them can drop a tagged line into the journal, which is the only part the audit will actually check.

The ticket

bash
sudo dnf install -y cronie at
sudo systemctl enable --now crond atd
systemctl is-active crond atd

Concept review. Three tools, three jobs.

bash
man 5 crontab
man at
man systemd.timer

A cron line has five time fields followed by the command. The order is the part people get wrong, so read it left to right every time.

cron format
* * * * *  command
| | | | |
| | | | +- day of week (0-7, Sunday is 0 or 7)
| | | +--- month (1-12)
| | +----- day of month (1-31)
| +------- hour (0-23)
+--------- minute (0-59)

There are shortcut strings too: @daily, @weekly, @monthly, @reboot.

cron for the daily backup

Edit root’s crontab and add one line. The percent signs inside the date command must be escaped with a backslash, because cron treats a bare percent as a newline.

bash
sudo crontab -e

# add this line (minute hour day month weekday):
30 2 * * * /usr/bin/tar -czf /srv/backups/etc-$(date +\%Y\%m\%d).tar.gz /etc/ 2>/dev/null && logger -t backup "daily etc backup completed"

sudo crontab -l
sudo journalctl -u crond -n 20

logger -t backup writes a tagged line into the journal, which is how you prove the job ran.

at for the one-shot update

at reads the command from stdin, so piping is the cleanest form. Queue it, then confirm with atq.

bash
echo "dnf upgrade -y && logger -t maint 'security upgrade complete'" | sudo at 23:00
# warning: commands will be executed using /bin/sh
# job 1 at Wed Jul 15 23:00:00 2026

sudo atq
# 1  Wed Jul 15 23:00:00 2026 a root
sudo at -c 1             # dumps the full script that will run
# sudo atrm 1            # remove it if plans change

Time formats at understands include 23:00, now + 2 hours, midnight, and 3pm Friday. If you give it a time that already passed today, it schedules for tomorrow, which surprises people at least once.

A systemd timer for the weekly cleanup

A timer is two unit files that share a name: a .service that does the work and a .timer that says when. The service is Type=oneshot because it runs and exits rather than staying up.

bash
sudo bash -c 'cat > /etc/systemd/system/backup-cleanup.service << "EOF"
[Unit]
Description=Delete backup tarballs older than 14 days

[Service]
Type=oneshot
ExecStart=/usr/bin/find /srv/backups -name "*.tar.*" -mtime +14 -delete
ExecStartPost=/usr/bin/logger -t backup-cleanup "completed"
EOF'

sudo bash -c 'cat > /etc/systemd/system/backup-cleanup.timer << "EOF"
[Unit]
Description=Run backup cleanup every Sunday at 4am

[Timer]
OnCalendar=Sun *-*-* 04:00:00
Persistent=true

[Install]
WantedBy=timers.target
EOF'

sudo systemctl daemon-reload
sudo systemctl enable --now backup-cleanup.timer
systemctl list-timers backup-cleanup.timer
# NEXT                        LEFT    UNIT                  ACTIVATES
# Sun 2026-07-19 04:00:00 EDT 3 days  backup-cleanup.timer  backup-cleanup.service

# test the service now rather than waiting until Sunday to find a typo
sudo systemctl start backup-cleanup.service
sudo journalctl -u backup-cleanup.service -n 10
note: Persistent=true means if the machine was off when the timer should have fired, it runs at the next boot instead of silently skipping.

cron time patterns worth reading aloud

cron patterns
0 0 * * *      daily at midnight
*/15 * * * *   every 15 minutes
0 9 * * 1-5    9 AM Monday through Friday
0 0 1 * *      first of every month at midnight
0 22 * * 0,6   10 PM on Saturday and Sunday

The systemd equivalents use OnCalendar: --* 05:00:00 is daily at 5, Mon --* 09:00:00 is Mondays at 9, and Mon..Fri --* 17:30:00 is weekdays at 5:30 PM.

Exam questions

Write the command first.

Q1. Schedule /usr/local/bin/backup.sh to run every Monday at 2:30 AM as the current user.

Q2. Schedule a one-time job to run dnf -y update tonight at 10:30 PM, and verify it is queued.

Q3. Create a systemd timer that runs /usr/local/bin/cleanup.sh every day at 5:00 AM, enabled on boot.

Answers.

bash
# A1: crontab -e, then add:
30 2 * * 1 /usr/local/bin/backup.sh
# order is minute hour day month weekday; 1 is Monday

# A2
echo "dnf -y update" | sudo at 22:30
sudo atq

# A3: two unit files, cleanup.service (Type=oneshot) and cleanup.timer
# timer: OnCalendar=*-*-* 05:00:00 and Persistent=true, then:
sudo systemctl daemon-reload
sudo systemctl enable --now cleanup.timer

Final checklist: confirm everything works

If every check passes, the ticket is done.

bash
# 1. daily 2:30 backup in root's crontab
sudo crontab -l

# 2. 11 PM upgrade queued
sudo atq

# 3. Sunday cleanup timer enabled
systemctl list-timers backup-cleanup.timer

# 4. all three test-fired without errors
# 5. all three exam commands written from scratch
# 6. tracker entry checked off

Reply to logadmin1: all three scheduled. Daily backup via cron, weekly cleanup via a systemd timer, tonight’s upgrade via at. Each logs to the journal with its own tag.

next post
Static IP, hostname, and DNS with nmcli