Automating rsync backups with cron: scheduling via crontab, directing output to a log file, understanding rsync exit codes (0 success, 23 partial), email notifications on failure, and lock files to prevent two backup processes from running at the same time.

A backup done manually is a backup that will never get done. Episode 9 gave you a snapshot script ready to run; episode 10 attaches it to cron — Linux's built-in scheduler — so the backup runs by itself every night without you typing anything.
But scheduling alone isn't enough. Reliable automation needs three things: logs (what happened), exit codes (did it succeed), and notifications (who knows when it fails). This episode builds all three.
Cron reads schedules from the crontab, formatted as five columns:
minute hour day-of-month month day-of-week commandOpen the crontab editor and add a backup entry:
crontab -e30 2 * * * /usr/local/bin/backup-rsync.sh >> /var/log/rsync.log 2>&130 2 * * * means every day at 02:30. That time is chosen because 2-3 AM is generally the quietest. >> /var/log/rsync.log 2>&1 directs stdout and stderr to a log file — a mandatory habit, because without a redirect cron sends output to a local email that's rarely read.
Don't put the entire rsync command directly in the crontab — wrap it in a script so you can add logic:
#!/bin/bash
LOG=/var/log/rsync.log
echo "=== $(date) rsync started ===" >> "$LOG"
rsync -avhP --partial-dir=.rsync-partial \
/home/data/ /backup/daily/ >> "$LOG" 2>&1
RC=$?
echo "=== $(date) rsync finished, exit=$RC ===" >> "$LOG"
exit $RCchmod +x /usr/local/bin/backup-rsync.sh then test it manually once before scheduling. This script writes timestamps to the log so each run is easy to trace.
Exit codes are rsync's language to cron. The values you'll encounter most often:
| Exit | Meaning |
|---|---|
0 | Success — all data synced |
1 | Syntax/usage error |
3 | Error protecting selected files |
5 | I/O error |
11 | File I/O error |
23 | Partial transfer — some files failed (files vanished/changed during transfer) |
24 | Some source files disappeared |
255 | Connection/SSH error |
23 is the code that most often confuses people: logically it's "not fully successful", and your script must treat it as a failure for notification purposes — but be aware that some data has already been transferred.
Warning
Exit code 0 means "no fatal error", not "nothing changed". Files that changed during transfer (vanished) produce exit 23/24 — the data is inconsistent. Don't ignore it; the log should contain the accompanying rsync warning message.
Sending email on failure turns a backup from "silent" into "monitored":
#!/bin/bash
LOG=/var/log/rsync.log
ADMIN=admin@example.com
rsync -avh /home/data/ /backup/daily/ >> "$LOG" 2>&1
RC=$?
if [ $RC -ne 0 ]; then
tail -50 "$LOG" | mail -s "Backup FAILED (exit $RC) - $(hostname)" "$ADMIN"
fi
exit $RCmail sends the last log summary to the admin address. On modern servers that rarely have an MTA, alternatives include: sendmail, curl to a webhook (Slack/Telegram), or the alerting integration in episode 20.
Imagine a nightly backup running longer than 24 hours (large dataset), then cron running a second one tomorrow night — two rsyncs writing to the same destination simultaneously. The result is corruption and scrambled logs. The solution is a lock file:
#!/bin/bash
exec 9>/var/lock/rsync-backup.lock
if ! flock -n 9; then
echo "$(date) another backup is still running, skipping." >> /var/log/rsync.log
exit 1
fi
rsync -avh /home/data/ /backup/daily/ >> /var/log/rsync.log 2>&1flock -n 9 locks file descriptor 9 non-blocking. If a previous backup is still running, the next cron run exits immediately with a skip message — no process pile-up. This pattern is mandatory for large dataset backups.
Tip
Combining flock + --timeout + --partial-dir (episode 7) makes cron backups almost never fail permanently: interrupted transfers get restarted, colliding ones get skipped, and hanging ones get cut off.
.bashrc/.profile. Use absolute paths (/usr/bin/rsync, /usr/bin/date) or set PATH=/usr/local/bin:/usr/bin:/bin at the top of your script.-e "ssh -i /root/.ssh/backup"); the agent from an interactive shell isn't available in cron. Details in episode 14.After writing the crontab, make sure the cron daemon is active and the entry is saved:
systemctl is-active cron
crontab -lcrontab -l displays the saved schedule. Also test the script manually first, then let cron take over — and don't forget to check the log the next morning.
In this episode you've automated rsync backups reliably.
Key takeaways:
>> /var/log/rsync.log 2>&1.0 success, 23 partial transfer — treat it as a failure.flock -n prevents two backups from running at once.In episode 11 we put all these skills to work on a big job: server migration & directory sync — moving home/var/www/database dumps between servers with --numeric-ids and -H (hardlinks), plus an honest discussion of rsync's limitations for two-way sync and its solution (unison). See you in episode 11!