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
| Issue | Description |
|---|---|
| No task dependencies | Can’t define “run B after A completes” |
| No priority system | All tasks are equal |
| Weak error handling | Only email or log on failure, no retry |
| No concurrency control | Concurrent execution can corrupt data |
| Hard debugging | No structured logging, must grep manually |
| Weak resource limits | Can’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) andOnBootSec/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,
journalctlfor 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 minutesto avoid this) - At sends results to user email by default; suppress with
> /dev/null 2>&1 - Ensure the
atdservice is running
# Check atd service status
sudo systemctl status atd
# Start atd service
sudo systemctl start atd
sudo systemctl enable atd
4. Comparison Matrix
| Feature | Cron | Systemd Timers | At |
|---|---|---|---|
| Scheduling type | Recurring | Recurring | One-shot |
| Precision | Minute | Microsecond | Minute |
| Log management | Email/manual | journalctl | |
| Resource control | None | Strong (Memory/CPU/I/O) | None |
| Concurrency control | None | Built-in | None |
| Failure retry | None | Yes | None |
| Dependency management | None | Yes | None |
| Learning curve | Low | Medium | Low |
| Best for | Simple recurring tasks | Critical/complex tasks | One-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
| Scenario | Recommended Tool |
|---|---|
| Simple periodic backup/cleanup | Cron |
| Critical tasks needing resource limits | Systemd Timers |
| One-time maintenance operations | At |
| Complex dependency chains | Systemd 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.
