Task Automation: Cron, At, and Systemd Timers
Scheduling tasks to run automatically is one of the most practical skills in system administration. Whether you need a backup script to run every night, a report to generate on the first of every month, or a one-time job to run at 3 AM, the tools covered on this page have you covered. We will look at cron (the classic scheduler), at and batch for one-time jobs, and systemd timers as a modern alternative with superior logging and reliability.
Cron Basics
Cron is a daemon that runs scheduled commands at specified times. You define your schedule in a crontab file, which you edit with crontab -e.
# Open your crontab for editing
crontab -e
# List your current crontab
crontab -l
# Remove your crontab (be careful!)
crontab -r
Cron Syntax
Each line in a crontab has five time fields followed by the command to run:
# min hr dom mon dow command
0 2 * * * /home/user/backup.sh
| Field | Range | Description |
|---|---|---|
| min | 0-59 | Minute |
| hr | 0-23 | Hour |
| dom | 1-31 | Day of month |
| mon | 1-12 | Month |
| dow | 0-7 | Day of week (0 and 7 are Sunday) |
# Every day at 2:00 AM
0 2 * * * /home/user/backup.sh
# Every 15 minutes
*/15 * * * * /usr/local/bin/health_check.sh
# Monday through Friday at 9:30 AM
30 9 * * 1-5 /home/user/morning_report.sh
# First day of every month at midnight
0 0 1 * * /home/user/monthly_cleanup.sh
# Every Sunday at 4:00 AM
0 4 * * 0 /home/user/weekly_maintenance.sh
# Twice a day at 8 AM and 8 PM
0 8,20 * * * /home/user/check_disk.sh
Special Strings
Instead of the five-field syntax, cron supports convenient shorthand strings.
@reboot /home/user/on_startup.sh # Run once at boot
@daily /home/user/daily_task.sh # Same as: 0 0 * * *
@weekly /home/user/weekly_task.sh # Same as: 0 0 * * 0
@monthly /home/user/monthly_task.sh # Same as: 0 0 1 * *
@yearly /home/user/yearly_task.sh # Same as: 0 0 1 1 *
@hourly /home/user/hourly_task.sh # Same as: 0 * * * *
Cron PATH Issues
One of the most common cron pitfalls is the PATH environment variable. Cron runs with a minimal PATH (typically just /usr/bin:/bin), so commands that work fine in your interactive shell may fail in cron because their binary locations are not included. This is the number one cause of "it works manually but not in cron" complaints.
# Solution 1: Use absolute paths in your crontab
0 2 * * * /usr/local/bin/restic backup /home
# Solution 2: Set PATH at the top of your crontab
PATH=/usr/local/bin:/usr/bin:/bin:/home/user/.local/bin
0 2 * * * restic backup /home
# Solution 3: Set PATH inside your script
#!/usr/bin/env bash
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
restic backup /home
Logging Cron Output
By default, cron emails output to the user. On systems without a mail transport agent (which is most modern servers), output is silently discarded. Redirect it explicitly to avoid losing error messages.
# Log stdout and stderr to a file
0 2 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1
# Discard output entirely
*/5 * * * * /home/user/ping_check.sh > /dev/null 2>&1
# Log errors only
0 3 * * * /home/user/cleanup.sh > /dev/null 2>> /var/log/cleanup_errors.log
Cron Environment Pitfalls
Beyond PATH, be aware that cron jobs run with a minimal environment. Variables like HOME, SHELL, USER, and LOGNAME are set, but nothing from your .bashrc or .profile is loaded. If your script depends on environment variables, set them explicitly:
# Set environment in the crontab
SHELL=/bin/bash
[email protected]
HOME=/home/deploy
Practical Crontab Example
# /tmp/crontab.example
# Environment
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
[email protected]
# Backups
0 2 * * * /home/deploy/backup_db.sh >> /var/log/backup_db.log 2>&1
30 2 * * * /home/deploy/backup_files.sh >> /var/log/backup_files.log 2>&1
# Monitoring
*/5 * * * * /home/deploy/health_check.sh > /dev/null 2>&1
# Cleanup
0 4 * * 0 /home/deploy/cleanup_tmp.sh >> /var/log/cleanup.log 2>&1
# Reports
30 9 * * 1 /home/deploy/weekly_report.sh 2>&1 | mail -s "Weekly Report" [email protected]
# SSL certificate renewal check
0 6 * * 1 /home/deploy/check_certs.sh >> /var/log/cert_check.log 2>&1
at and batch: One-Time Jobs
While cron handles recurring schedules, at runs a command once at a specified time. batch runs a command when the system load drops below a threshold (typically 1.5).
# Run a command at a specific time
at 3:00 AM
> /home/user/one_time_migration.sh
> <Ctrl-D>
# Schedule from the command line
echo "/home/user/deploy.sh" | at 14:30
# Relative time
at now + 2 hours <<'EOF'
/home/user/reminder.sh
EOF
at now + 30 minutes <<'EOF'
echo "Check the deployment" | mail -s "Reminder" [email protected]
EOF
# Tomorrow
at 9:00 AM tomorrow <<'EOF'
/home/user/morning_task.sh
EOF
# List pending at jobs
atq
# Remove a pending job (use job number from atq)
atrm 5
# batch: runs when system load is low (default: load < 1.5)
echo "/home/user/heavy_task.sh" | batch
The at command is useful for scheduling one-off maintenance tasks, delayed notifications, or any job that should run exactly once at a specific time.
Systemd Timers
On systems running systemd (most modern Linux distributions), timer units are a powerful alternative to cron. They offer better logging (via journald), dependency management, resource controls, and the ability to run missed jobs.
Anatomy of a Timer
A systemd timer consists of two files: a .timer unit and a corresponding .service unit.
# /etc/systemd/system/backup.service
[Unit]
Description=Daily database backup
[Service]
Type=oneshot
ExecStart=/home/deploy/backup_db.sh
User=deploy
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Key Timer Options
| Option | Description |
|---|---|
OnCalendar= |
Calendar-based schedule (like cron) |
OnBootSec= |
Time after boot (e.g., 5min) |
OnUnitActiveSec= |
Time after the unit was last activated |
Persistent=true |
Run missed executions (like anacron) |
RandomizedDelaySec= |
Add random delay to prevent thundering herd |
AccuracySec= |
Timer accuracy (default 1min, set to 1s for precision) |
OnCalendar Syntax
The OnCalendar syntax is more readable than cron and supports a wider range of expressions.
# Every day at 2 AM
OnCalendar=*-*-* 02:00:00
# Every Monday at 9:30 AM
OnCalendar=Mon *-*-* 09:30:00
# First of every month at midnight
OnCalendar=*-*-01 00:00:00
# Every 15 minutes
OnCalendar=*:0/15
# Twice a day
OnCalendar=*-*-* 08,20:00:00
# Validate your expression
systemd-analyze calendar "*-*-* 02:00:00"
The systemd-analyze calendar command is invaluable for testing. It shows you when the next trigger will occur, eliminating guesswork.
Enabling and Managing Timers
# Reload systemd after creating/modifying unit files
sudo systemctl daemon-reload
# Enable and start the timer
sudo systemctl enable --now backup.timer
# Check timer status
systemctl status backup.timer
# List all active timers
systemctl list-timers --all
# Manually trigger the associated service (for testing)
sudo systemctl start backup.service
# View logs
journalctl -u backup.service -f
# View logs since last boot
journalctl -u backup.service -b
Persistent=true
One of the biggest advantages of systemd timers over cron is the Persistent=true option. If the system was powered off when a scheduled run should have occurred, systemd will run the job immediately upon boot. Cron has no equivalent (you would need anacron for this behavior). This is critical for backup scripts and maintenance tasks on machines that are not always running.
Cron vs Systemd Timers
| Feature | Cron | Systemd Timer |
|---|---|---|
| Availability | Everywhere | systemd-based Linux only |
| Logging | Must configure manually | journald (built in) |
| Missed runs | Lost (unless using anacron) | Persistent=true |
| Resource limits | None | cgroups via [Service] |
| Dependencies | None | full systemd dependency graph |
| Ease of setup | One line | Two files |
| Debugging | Difficult | systemctl status, journalctl |
| Boot-time jobs | @reboot |
OnBootSec= |
For simple recurring tasks on any UNIX system, cron is hard to beat -- it is universal, simple, and well-understood. For production Linux servers where you want logging, reliability, and resource controls, systemd timers are the better choice.
A Complete Systemd Timer Example
Here is a real-world example: a timer that cleans up old Docker images every Sunday at 4 AM with resource limits to prevent the cleanup from impacting production workloads.
# /etc/systemd/system/docker-cleanup.service
[Unit]
Description=Clean up unused Docker images
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
ExecStart=/usr/bin/docker image prune -af --filter "until=168h"
CPUQuota=50%
MemoryMax=512M
# /etc/systemd/system/docker-cleanup.timer
[Unit]
Description=Weekly Docker image cleanup
[Timer]
OnCalendar=Sun *-*-* 04:00:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now docker-cleanup.timer
systemctl list-timers | grep docker
Next Steps
Automated tasks often involve advanced scripting techniques. See Advanced Bash for functions, traps, and error handling that make your scheduled scripts robust. For general command-line workflow improvements, visit CLI Productivity. Return to the Shell Scripting hub for the complete topic list.