Featured image of post VPS Task Scheduling Guide: Cron vs Systemd Timers vs At — Complete Comparison

VPS Task Scheduling Guide: Cron vs Systemd Timers vs At — Complete Comparison

Cron is the classic scheduling tool, Systemd Timers is the modern Linux alternative, and At is for one-time tasks. This article compares all three in depth with practical examples to help you choose the right scheduler for every scenario

Introduction

Scheduled tasks and background jobs are everywhere in VPS operations:

  • Daily database backups at 2 AM
  • Hourly disk space checks
  • Weekly log rotation
  • One-time migration scripts
  • Automatic service restarts on failure

Which tool do you use? Cron? Systemd Timers? Or manual nohup?

Different scheduling scenarios need different tools. Cron is simple but lacks modern features. Systemd Timers are powerful but slightly more complex. At is great for one-shot tasks. This article provides a comprehensive comparison to help you build a solid scheduling strategy.


1. Cron: The Classic Task Scheduler

1.1 Basic Syntax

The core of Cron is a text file (crontab), where each line represents one task:

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of week (0 - 6, 0=Sunday)
# │ │ │ │ │
# * * * * *  command to execute

Common examples:

# Daily database backup at 2:00 AM
0 2 * * * /usr/bin/mysqldump -u root mydb | gzip > /backup/db_$(date +\%Y\%m\%d).sql.gz

# Check disk every 5 minutes
*/5 * * * * /usr/local/bin/check_disk.sh

# Clean logs every Monday at 9:00 AM
0 9 * * 1 /usr/bin/find /var/log -mtime +30 -delete

# Clear cache on the 1st of every month at 3:30 AM
30 3 1 * * /usr/bin/clear_cache.sh

# Sync time at 8:00 AM and 8:00 PM daily
0 8,20 * * * /usr/sbin/ntpdate pool.ntp.org

1.2 Management Commands

# Edit current user's crontab
crontab -e

# List current crontab
crontab -l

# Remove current crontab
crontab -r

# View system-level cron tasks
sudo cat /etc/crontab

# Check system cron directories
ls /etc/cron.d/
ls /etc/cron.daily/
ls /etc/cron.hourly/
ls /etc/cron.weekly/
ls /etc/cron.monthly/

1.3 Environment Variables Pitfall

Cron has very limited environment variables — a common source of bugs:

# Set environment variables explicitly in crontab
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com

# Daily 2 AM backup
0 2 * * * /usr/bin/mysqldump -u root mydb > /backup/db.sql

Important: Cron uses /bin/sh by default, not /bin/bash. If your script uses bash features (arrays, [[ ]], etc.), always set SHELL=/bin/bash at the top of your crontab.

1.4 Cron Limitations

IssueDescription
No task dependenciesCan’t define “run B after A completes”
No priority systemAll tasks are equal
Weak error handlingOnly email or log on failure, no retry
No concurrency controlConcurrent execution can corrupt data
Hard debuggingNo structured logging, must grep manually
Weak resource limitsCan’t precisely cap CPU/memory usage

2. Systemd Timers: Modern Linux Task Scheduler

2.1 Key Advantages

Systemd Timers are the systemd-provided scheduling mechanism with these advantages over Cron:

  • Unified service management: Timer and Service are bound, easy to start/stop/monitor
  • Precise scheduling: Supports OnCalendar (calendar-based) and OnBootSec/OnUnitActiveSec (relative time)
  • Resource limits: Can set CPU/memory limits directly in Service
  • Dependency management: Define preconditions and post-actions
  • Log integration: Output goes straight to journal, journalctl for one-click viewing
  • Concurrency control: Prevents overlapping execution by default (Persist=true)
  • Failure retry: Configurable retry strategies

2.2 Creating a Timer Task

Example: Run database backup daily at 2 AM.

Step 1: Create the Service unit file

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

[Service]
Type=oneshot
User=postgres
WorkingDirectory=/opt/backup
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/usr/bin/pg_dump mydb | gzip > /opt/backup/db_$(date +\%Y\%m\%d).sql.gz
StandardOutput=journal
StandardError=journal

# Resource limits
MemoryMax=512M
CPUQuota=50%

# Failure retry
Restart=on-failure
RestartSec=60
StartLimitIntervalSec=600
StartLimitBurst=3

Step 2: Create the Timer unit file

# /etc/systemd/system/db-backup.timer
[Unit]
Description=Daily Database Backup Timer

[Timer]
# Trigger at 2:00 AM daily
OnCalendar=*-*-* 02:00:00
# Delay up to 5 minutes (avoid mass startup)
RandomizedDelaySec=5min
# Catch up if machine was asleep
Persistent=true
# Also run 5 minutes after boot
OnBootSec=5min

[Install]
WantedBy=timers.target

Step 3: Enable and start

# Reload systemd configuration
sudo systemctl daemon-reload

# Enable Timer (auto-start on boot)
sudo systemctl enable db-backup.timer

# Start Timer
sudo systemctl start db-backup.timer

# Check Timer status
sudo systemctl status db-backup.timer

# Check next trigger time
sudo systemctl list-timers --all

2.3 Common Timer Expressions

# ── Absolute time (OnCalendar) ──

# Every minute
OnCalendar=*:0/1

# Every 5 minutes
OnCalendar=*:0/5

# At minute 15 of every hour
OnCalendar=*-*-* *:15:00

# Daily at 2:30 AM
OnCalendar=*-*-* 02:30:00

# Every Monday at 9:00 AM
OnCalendar=Mon *-*-* 09:00:00

# 1st of every month at 3:00 AM
OnCalendar=01 *-* 03:00:00

# First day of each quarter
OnCalendar=*-01,04,07,10-01 00:00:00

# Every Saturday at midnight
OnCalendar=Sat *-*-* 00:00:00

# Weekdays in 2026
OnCalendar=Mon..Fri *-*-* 09:00:00

# ── Relative time (after boot/activation) ──

# 5 minutes after boot
OnBootSec=5min

# 1 hour after last activation
OnUnitActiveSec=1h

# 30 minutes after last stop
OnUnitInactiveSec=30min

2.4 Best Practices

# List all timers with status
systemctl list-timers --all --no-pager

# View timer trigger history
journalctl -u db-backup.timer --since "24 hours ago"

# View executed service logs
journalctl -u db-backup.service --since "24 hours ago"

# Manually trigger once (for testing)
systemctl start db-backup.service

# Check next elapse time
systemctl show db-backup.timer --property=NextElapseUSecRealtime

3. At: One-Shot Scheduled Tasks

3.1 Use Cases

At is designed specifically for one-time scheduled tasks, not recurring schedules:

  • Send a reminder email in 30 minutes
  • Clean temp files at 11 PM tonight
  • Restart a service tomorrow morning
  • Pause a service before a maintenance window

3.2 Basic Usage

# List pending at tasks
atq

# Remove an at task
atrm <job-id>

# Run cleanup at 11 PM tonight
echo "/usr/bin/find /tmp -mtime +1 -delete" | at 23:00

# Restart a service tomorrow at 8 AM
echo "systemctl restart nginx" | at 8:00 tomorrow

# Run in 30 minutes
echo "/usr/local/bin/backup.sh" | at now + 30 minutes

# Next Monday at 9 AM
echo "run_migration.sh" | at 9:00 Monday

# December 31, 2026 at 23:59
echo "countdown.sh" | at 23:59 12/31/2026

3.3 Notes

  • At tasks execute once and are automatically removed
  • If the target time has passed, the task runs at end of day (use now + N minutes to avoid this)
  • At sends results to user email by default; suppress with > /dev/null 2>&1
  • Ensure the atd service is running
# Check atd service status
sudo systemctl status atd

# Start atd service
sudo systemctl start atd
sudo systemctl enable atd

4. Comparison Matrix

FeatureCronSystemd TimersAt
Scheduling typeRecurringRecurringOne-shot
PrecisionMinuteMicrosecondMinute
Log managementEmail/manualjournalctlEmail
Resource controlNoneStrong (Memory/CPU/I/O)None
Concurrency controlNoneBuilt-inNone
Failure retryNoneYesNone
Dependency managementNoneYesNone
Learning curveLowMediumLow
Best forSimple recurring tasksCritical/complex tasksOne-time operations

5. Practical Combinations

5.1 Scenario 1: Daily Database Backup

# /etc/systemd/system/db-backup.timer
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=5min

# /etc/systemd/system/db-backup.service
[Service]
Type=oneshot
ExecStart=/opt/scripts/db-backup.sh
MemoryMax=1G
CPUQuota=80%

5.2 Scenario 2: Hourly Health Check

# /etc/systemd/system/health-check.timer
[Timer]
OnCalendar=*:0/60
Persistent=true

# /etc/systemd/system/health-check.service
[Service]
Type=oneshot
ExecStart=/opt/scripts/health-check.sh
StandardOutput=journal
StandardError=journal

5.3 Scenario 3: One-Time Maintenance

# Restart service and clean cache at 3 AM tomorrow
echo "systemctl restart app && rm -rf /tmp/*" | at 3:00 tomorrow

5.4 Scenario 4: Dependency Chain

# /etc/systemd/system/backup-chain.timer
[Timer]
OnCalendar=*-*-* 01:00:00
Persistent=true

# /etc/systemd/system/backup-chain.service
[Unit]
Requires=db-backup.service
After=db-backup.service

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup-chain.sh

6. Troubleshooting

6.1 Cron Task Not Running

# Check if cron service is running
sudo systemctl status cron

# Check for syntax errors
sudo crontab -l

# Check cron logs
sudo grep CRON /var/log/syslog
sudo journalctl -u cron --since "24 hours ago"

# Common pitfall: use absolute paths
/usr/bin/python3 /opt/scripts/task.py

6.2 Systemd Timer Not Firing

# Check timer status
systemctl status db-backup.timer

# Check next trigger time
systemctl show db-backup.timer --property=NextElapseUSecRealtime

# Verify service syntax
systemd-analyze verify db-backup.service

# View full logs
journalctl -u db-backup.timer -u db-backup.service --since "1 hour ago"

6.3 At Task Not Executing

# Check atd service
sudo systemctl status atd

# List pending queue
atq

# Check executed history
grep at-agent /var/log/syslog

7. Advanced Tips

7.1 Hybrid Approach

For most scenarios, the recommended approach is:

  • Simple recurring tasks → Cron
  • Critical business tasks → Systemd Timers
  • One-time maintenance → At

7.2 Replace Polling with Event Listeners

Instead of checking file changes every minute with Cron, use inotify:

# Install fswatch
sudo apt install fswatch

# Trigger script on file change
fswatch -o /var/www/html | while read; do
    /opt/scripts/deploy.sh
done

7.3 Timeout Control

# Use timeout in Cron
*/5 * * * * timeout 300 /usr/local/bin/long-running-task.sh

# Set timeout in Systemd
[Service]
TimeoutStartSec=300
TimeoutStopSec=60

7.4 Concurrency Control

# Cron: use flock
*/5 * * * * flock -n /tmp/mytask.lock /usr/local/bin/mytask.sh

# Systemd: built-in overlap prevention
[Timer]
Persistent=true
# Systemd prevents concurrent execution of the same unit by default

Summary

ScenarioRecommended Tool
Simple periodic backup/cleanupCron
Critical tasks needing resource limitsSystemd Timers
One-time maintenance operationsAt
Complex dependency chainsSystemd Timers
High-frequency checks (sub-minute)Systemd Timers

Core principle: Use Cron for simple tasks, Systemd Timers for critical ones, and At for one-shot operations. Combine all three tools flexibly based on task importance, complexity, and scheduling needs to build a robust VPS automation operations system.

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials