Systemd timers are the modern Linux alternative to cron jobs — they provide per-task logging via journald, dependency management between services, accurate scheduling with calendar-based syntax, and automatic retries when a task fails, all without the silent failure mode that makes cron jobs notoriously difficult to debug. A systemd timer unit (.timer) triggers a corresponding service unit (.service), which defines the command to run, the user to run it as, and resource limits. Calendar events use the systemd OnCalendar syntax (*-*-* 03:00:00 for daily at 3 AM, Mon *-*-* 06:00:00 for weekly on Monday) which is more readable than cron’s five-field format and supports expressions like weekly, monthly, and hourly as shorthand. RandomizedDelaySec adds a random jitter to the trigger time, distributing the load when many timers would otherwise fire simultaneously on a server hosting multiple WordPress sites. Persistent=true on a timer causes it to run immediately if the last scheduled run was missed (e.g., the server was off during the scheduled window) — this is the correct behavior for WordPress cron-like tasks that must eventually run even if delayed. All output from a systemd service is automatically captured in the system journal — journalctl -u wordpress-backup.service --since "1 hour ago" shows the complete output of the last backup run, including any PHP errors or WP-CLI warnings, without configuring any log redirection in the script itself. The wp cron event run --due-now WP-CLI command can be triggered by a systemd timer to replace the HTTP-based wp-cron mechanism with a reliable system-level scheduler, fixing the common problem where wp-cron events are missed on low-traffic sites because no HTTP requests arrive to trigger them. The mysqldump cron post shows the backup script that pairs with the service unit below.
Problem: A WordPress server has seven cron jobs defined in crontab — database backups, cache clearing, log rotation, and WP-CLI cron processing — and when one silently fails there is no notification or log trail to diagnose the failure; jobs pile up, backups stop, and the problem is discovered days later.
Solution: Convert each cron job to a systemd timer + service pair, enable journald logging automatically, add OnFailure= to send an email alert when a job fails, and use Persistent=true so missed jobs catch up after server reboots.
# ── 1. WordPress WP-Cron replacement timer ───────────────────────────────────
# /etc/systemd/system/wp-cron.service
[Unit]
Description=WordPress WP-Cron event runner
After=network.target mysql.service
[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/html
ExecStart=/usr/local/bin/wp --path=/var/www/html cron event run --due-now --quiet
StandardOutput=journal
StandardError=journal
# Send email via a notification service if this unit fails
OnFailure=systemd-email@%n.service
# /etc/systemd/system/wp-cron.timer
[Unit]
Description=Run WordPress WP-Cron every 5 minutes
Requires=wp-cron.service
[Timer]
OnCalendar=*:0/5 # every 5 minutes
RandomizedDelaySec=30 # spread load within the 5-minute window
Persistent=true # run immediately if a trigger was missed
[Install]
WantedBy=timers.target
# ── 2. Daily database backup timer ───────────────────────────────────────────
# /etc/systemd/system/wp-backup.service
[Unit]
Description=WordPress daily database backup
After=mysql.service
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/wp-backup.sh
StandardOutput=journal
StandardError=journal
# Restrict resources so backup cannot starve the web server
CPUQuota=25%%
MemoryMax=256M
OnFailure=systemd-email@%n.service
# /etc/systemd/system/wp-backup.timer
[Unit]
Description=Daily WordPress backup at 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=600 # run anywhere in the 03:00–03:10 window
Persistent=true
[Install]
WantedBy=timers.target
# ── 3. Enable and manage timers ───────────────────────────────────────────────
sudo systemctl daemon-reload
sudo systemctl enable --now wp-cron.timer
sudo systemctl enable --now wp-backup.timer
# List all active timers and next trigger times
systemctl list-timers --all | grep wp-
# View logs from the last backup run
journalctl -u wp-backup.service -n 50 --no-pager
# Manually trigger a timer's service for testing
sudo systemctl start wp-backup.service
echo "Exit code: $?"
# ── 4. Disable wp-cron HTTP triggering in wp-config.php ──────────────────────
# (Add this after converting to systemd-based cron)
# define( 'DISABLE_WP_CRON', true );
NOTE: After creating timer and service unit files, always run sudo systemctl daemon-reload before enabling them — systemd reads unit files at load time, so changes to unit files are invisible until the daemon reloads. To verify a timer will trigger at the expected time without waiting, use systemd-analyze calendar "*-*-* 03:00:00" which parses the calendar expression and prints the next five trigger times, catching typos in the schedule syntax before deployment.