Shell Scripting systemd Timers Intermediate May 2026

Shell Scripting systemd Timers

Replace cron with systemd timers for better logging, dependency management, and missed-run handling. Learn service unit files, timer units, OnCalendar syntax, and journalctl log inspection.

systemd timers are the modern replacement for cron on Linux systems. They offer better logging via journalctl, can depend on other systemd units, handle missed runs (like anacron), run as any user, and integrate with the entire service lifecycle. On any systemd-based server (Ubuntu 16+, CentOS 7+, Debian 8+), timers are the recommended approach for new automation.

Every timer needs two unit files: a service unit (what to run) and a timer unit (when to run it). They share the same name — myapp-backup.service and myapp-backup.timer.

INI
# /etc/systemd/system/myapp-backup.service
[Unit]
Description=MyApp Daily Database Backup
After=network.target mysql.service

[Service]
Type=oneshot
User=vriddh
Group=vriddh
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/scripts/db_backup.sh
StandardOutput=journal
StandardError=journal
Environment="DB_HOST=prod-db-01"
Environment="BACKUP_DIR=/backups"

[Install]
WantedBy=multi-user.target
INI
# /etc/systemd/system/myapp-backup.timer
[Unit]
Description=MyApp Daily Database Backup Timer
Requires=myapp-backup.service

[Timer]
OnCalendar=*-*-* 02:00:00          # daily at 02:00
RandomizedDelaySec=300             # spread load ±5 min
Persistent=true                    # run if missed (like anacron)
Unit=myapp-backup.service

[Install]
WantedBy=timers.target
INI
# OnCalendar format: DayOfWeek Year-Month-Day Hour:Minute:Second

# ── Common schedules ──────────────────────────────────────
OnCalendar=minutely               # every minute
OnCalendar=hourly                 # every hour
OnCalendar=daily                  # daily at midnight
OnCalendar=weekly                 # weekly on Monday
OnCalendar=monthly                # first of month

# ── Specific times ────────────────────────────────────────
OnCalendar=*-*-* 02:30:00         # every day at 02:30
OnCalendar=Mon *-*-* 09:00:00    # every Monday at 09:00
OnCalendar=Mon..Fri *-*-* 08:00:00 # weekdays at 08:00
OnCalendar=*-*-1 00:00:00        # 1st of every month
OnCalendar=*-1,7-1 00:00:00      # Jan 1 and Jul 1

# ── Every N minutes/hours ─────────────────────────────────
OnCalendar=*:0/5                  # every 5 minutes
OnCalendar=*:0/15                 # every 15 minutes
OnCalendar=0/2:00:00              # every 2 hours

# ── Relative triggers (not calendar-based) ────────────────
OnBootSec=30s                     # 30 seconds after boot
OnUnitActiveSec=1h                # 1h after last run
OnUnitInactiveSec=1d              # 1 day after last completion

# Test a calendar expression before using it:
systemd-analyze calendar "Mon..Fri *-*-* 08:00:00"
BASH
# ── After creating unit files ─────────────────────────────
systemctl daemon-reload             # reload systemd config
systemctl enable --now myapp-backup.timer  # enable and start timer

# ── Managing timers ───────────────────────────────────────
systemctl start myapp-backup.timer   # start timer
systemctl stop myapp-backup.timer    # stop timer
systemctl disable myapp-backup.timer # disable from auto-start
systemctl status myapp-backup.timer  # check status + next run time

# ── Run the service immediately (test without waiting) ────
systemctl start myapp-backup.service  # run now, manually

# ── List all timers ───────────────────────────────────────
systemctl list-timers               # active timers + next trigger
systemctl list-timers --all         # include inactive

# ── View logs ─────────────────────────────────────────────
journalctl -u myapp-backup.service  # all logs for service
journalctl -u myapp-backup.service -n 50  # last 50 lines
journalctl -u myapp-backup.service --since "1 hour ago"
journalctl -u myapp-backup.service -f  # follow live
journalctl -u myapp-backup.service --since today  # today only
bash — systemd timer management
vriddh@prod-01:~$systemctl list-timers myapp-backup.timer
NEXT LEFT LAST PASSED UNIT
Fri 2026-05-02 02:00:00 UTC 13h left Thu 2026-05-01 02:03:42 UTC 11h ago myapp-backup.timer
vriddh@prod-01:~$journalctl -u myapp-backup.service --since today
May 01 02:03:41 prod-01 systemd[1]: Starting MyApp Daily Database Backup...
May 01 02:03:41 prod-01 db_backup.sh[18421]: [2026-05-01 02:03:41] Starting backup
May 01 02:03:42 prod-01 db_backup.sh[18421]: [2026-05-01 02:03:42] Backup complete: 247MB
May 01 02:03:42 prod-01 systemd[1]: myapp-backup.service: Deactivated successfully.
May 01 02:03:42 prod-01 systemd[1]: Finished MyApp Daily Database Backup.
BASH
# ── systemd timers advantages ──────────────────────────────
# ✔ Full journald logging — no output lost, easy to query
# ✔ Run missed jobs on next boot (Persistent=true)
# ✔ Depend on other units (After=network.target)
# ✔ Better environment control (Environment= directives)
# ✔ Can be controlled with systemctl start/stop
# ✔ Status shows next and last run times
# ✔ Security sandboxing (ProtectSystem, NoNewPrivileges etc)

# ── cron advantages ────────────────────────────────────────
# ✔ Simpler — one line per job
# ✔ User-level (no root needed for crontab -e)
# ✔ Works on non-systemd systems
# ✔ Everyone already knows it

# ── Recommendation ────────────────────────────────────────
# Use systemd timers for: system-level jobs, production services,
#   anything needing dependencies or security sandboxing
# Use cron for: user-level jobs, simple scripts, quick automation

# Verify next timer run time
systemd-analyze calendar "*-*-* 02:00:00"
# Output:
#   Original form: *-*-* 02:00:00
#   Normalized form: *-*-* 02:00:00
#   Next elapse: Fri 2026-05-02 02:00:00 UTC
✔ systemd timer checklist — Always set Persistent=true if the job must not be skipped. Always set RandomizedDelaySec on multi-server deployments to spread load. Use Type=oneshot in the service for scripts that run and exit. Use journalctl -u servicename — not /var/log/syslog — to view logs. Run systemd-analyze calendar to validate your schedule before deploying.