TL;DR — Quick Summary

rsync guide: delta transfer algorithm, essential flags, SSH remote backups, --link-dest incremental snapshots, cron automation, and daemon mode on Linux.

rsync is the standard Linux tool for efficient file copying and remote backup. Its delta transfer algorithm computes checksums on both sides and sends only the bytes that differ — making it dramatically faster than scp for repeated transfers of large directory trees. This guide covers everything from basic flags through production incremental backup scripts with --link-dest rotation.

Prerequisites

  • Linux system (Ubuntu 22.04+, Debian 12+, RHEL 9+, or any modern distro).
  • rsync installed on source and destination machines (sudo apt install rsync / sudo dnf install rsync).
  • SSH key-based authentication configured for remote transfers.
  • Root or sudo access for system-level backups.

How rsync Works

rsync uses the delta transfer algorithm to minimise data sent over the wire:

  1. The sender lists files and computes rolling checksums of small blocks.
  2. The receiver checks which blocks it already has.
  3. Only missing or changed blocks are transmitted.
  4. Metadata (permissions, timestamps, ownership) is synchronised separately.

For a large directory that changes only 1% per day, rsync typically transfers less than 1% of the total data on subsequent runs. The first transfer is always a full copy; every run after that is incremental by default.

Key properties:

  • Checksum-based — detects changes even if the modification time was not updated.
  • Atomic-like — files are written to a temporary name and renamed on completion.
  • Cross-platform — runs on Linux, macOS, and Windows (via WSL or Cygwin).
  • Protocol version negotiated — client and server agree on the highest supported version at connect time.

Basic Syntax

rsync [OPTIONS] SOURCE DESTINATION
PatternMeaning
rsync -a /src/ /dst/Copy contents of /src/ into /dst/
rsync -a /src /dst/Copy the /src directory itself into /dst/
rsync -a user@host:/remote/ /local/Pull from remote to local
rsync -a /local/ user@host:/remote/Push from local to remote

The trailing slash on the source matters. /src/ means “copy the contents of src”; /src means “copy the directory src itself into the destination.”


Essential Flags

rsync -avzP --delete \
  --exclude='*.tmp' \
  --exclude='.cache/' \
  /home/user/ user@backupserver:/backups/home/
FlagEffect
-aArchive: recursive + preserve permissions, ownership, timestamps, symlinks, devices
-vVerbose: print each file transferred
-zCompress data during transfer (useful on slow links; skip on fast LAN)
-PShow per-file progress + keep partial files (--progress --partial combined)
--deleteDelete destination files absent from source — creates an exact mirror
-n / --dry-runSimulate the transfer without making any changes
--exclude=PATTERNSkip files matching the pattern
--include=PATTERNForce-include files even if a broader exclude would skip them
-e sshUse SSH as the transport (default in modern rsync; add options with -e 'ssh -p 2222')
--checksumCompare file contents (not just size+mtime); slower but catches silent corruption
--bwlimit=KBPSCap bandwidth usage (e.g. --bwlimit=10000 for 10 MB/s)
--log-file=PATHWrite a transfer log to the specified file
--statsPrint summary statistics after transfer completes

Always dry-run first

rsync -avzn --delete /important/ user@host:/backup/important/

Review the output, then re-run without -n to apply the changes.


Remote Sync over SSH

Push (local → remote)

rsync -avz -e "ssh -i ~/.ssh/backup_ed25519" \
  /var/www/html/ \
  backupuser@192.168.1.50:/backups/webroot/

Pull (remote → local)

rsync -avz -e "ssh -i ~/.ssh/backup_ed25519" \
  backupuser@192.168.1.50:/backups/webroot/ \
  /var/www/html/

Custom SSH port

rsync -avz -e "ssh -p 2222" /src/ user@host:/dst/

Restrict the backup SSH key

On the backup server, in ~/.ssh/authorized_keys, prefix the public key with a forced command so it can only run rsync — not an interactive shell:

command="rsync --server --daemon .",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAA...key...

This limits the damage if the client machine is compromised.


Bandwidth Limiting

On production systems, unrestricted rsync can saturate a link. --bwlimit accepts kilobytes per second:

# Limit to 5 MB/s
rsync -avz --bwlimit=5000 /source/ user@host:/dest/

# Limit to 1 MB/s for background backup jobs
rsync -avz --bwlimit=1000 --delete /home/ user@host:/backups/home/

For more dynamic throttling, combine rsync with ionice and nice to reduce its impact on the CPU and disk scheduler:

nice -n 19 ionice -c 3 rsync -avz --bwlimit=2000 /source/ user@host:/dest/

Preserving Permissions, Ownership, and Extended Attributes

The -a (archive) flag is a shorthand for -rlptgoD:

Sub-flagPreserves
-rRecursion
-lSymlinks as symlinks
-pPermissions (chmod bits)
-tModification timestamps
-gGroup ownership
-oUser ownership (requires root)
-DDevice files and special files

For ACLs and extended attributes (needed for SELinux contexts or macOS xattrs):

rsync -aAX /source/ /destination/
Extra flagPreserves
-APOSIX ACLs
-XExtended attributes (xattrs)

On SELinux systems, add --rsync-path="sudo rsync" on the remote side when rsync needs elevated access to read or write xattrs.


--link-dest enables hardlink-based snapshot backups — each daily directory looks like a complete copy of your data but only new or changed files occupy extra space. Unchanged files are hard-linked from the previous snapshot.

Concept

/backups/
  2026-03-20/   ← full data, 50 GB
  2026-03-21/   ← hard-linked unchanged files + only changed files; uses ~200 MB extra
  2026-03-22/   ← hard-linked from 2026-03-21 + today's changes; uses ~150 MB extra

Script

#!/bin/bash
set -euo pipefail

SOURCE="/home/"
DEST="backupuser@192.168.1.50:/backups"
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
SSH_KEY="/root/.ssh/backup_ed25519"

rsync -avz \
  -e "ssh -i $SSH_KEY" \
  --link-dest="$DEST/daily/$YESTERDAY" \
  --delete \
  "$SOURCE" \
  "$DEST/daily/$TODAY/"

echo "Backup complete: $TODAY"

Rotation: daily / weekly / monthly

#!/bin/bash
# Run daily. Keeps 7 daily, 4 weekly (Sunday), 12 monthly (1st of month).

SOURCE="/home/"
DEST_BASE="/backups"
TODAY=$(date +%Y-%m-%d)
DOW=$(date +%u)     # 1=Mon ... 7=Sun
DOM=$(date +%d)     # day of month

DAILY_DEST="$DEST_BASE/daily/$TODAY"
LATEST_LINK="$DEST_BASE/daily/latest"

rsync -a --delete \
  --link-dest="$LATEST_LINK" \
  "$SOURCE" "$DAILY_DEST/"

# Update the 'latest' symlink
ln -sfn "$DAILY_DEST" "$LATEST_LINK"

# Weekly snapshot on Sunday
if [ "$DOW" -eq 7 ]; then
  cp -al "$DAILY_DEST" "$DEST_BASE/weekly/$(date +%Y-W%V)"
fi

# Monthly snapshot on the 1st
if [ "$DOM" -eq 01 ]; then
  cp -al "$DAILY_DEST" "$DEST_BASE/monthly/$(date +%Y-%m)"
fi

# Prune: keep last 7 daily, 4 weekly, 12 monthly
find "$DEST_BASE/daily"   -maxdepth 1 -type d | sort | head -n -7  | xargs rm -rf
find "$DEST_BASE/weekly"  -maxdepth 1 -type d | sort | head -n -4  | xargs rm -rf
find "$DEST_BASE/monthly" -maxdepth 1 -type d | sort | head -n -12 | xargs rm -rf

Automation: cron and systemd Timers

cron

crontab -e
# Add:
0 2 * * * /usr/local/bin/rsync-backup.sh >> /var/log/rsync-backup.log 2>&1

systemd service + timer

/etc/systemd/system/rsync-backup.service:

[Unit]
Description=Daily rsync backup
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/rsync-backup.sh
StandardOutput=journal
StandardError=journal

/etc/systemd/system/rsync-backup.timer:

[Unit]
Description=Daily rsync backup timer

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=10min
Persistent=true

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now rsync-backup.timer
systemctl list-timers rsync-backup.timer

Systemd timers are preferred over cron because they log to journald, support Persistent=true (catches missed runs after downtime), and integrate with dependency ordering.


rsync Daemon Mode

For high-volume or scheduled pulls without interactive SSH authentication, rsync can run as a standalone daemon.

/etc/rsyncd.conf

# Global settings
uid = nobody
gid = nogroup
use chroot = yes
max connections = 4
log file = /var/log/rsyncd.log
pid file = /var/run/rsyncd.pid

# Module definition
[backups]
    path = /backups/shared
    comment = Shared backup area
    read only = no
    auth users = backupclient
    secrets file = /etc/rsyncd.secrets
    hosts allow = 192.168.1.0/24
    hosts deny = *

/etc/rsyncd.secrets

backupclient:strongpassword123
chmod 600 /etc/rsyncd.secrets

Start the daemon

# systemd
systemctl enable --now rsync

# Or standalone
rsync --daemon

Client usage

rsync -avz backupclient@backupserver::backups /local/restore/

The double-colon (::) signals daemon mode (as opposed to SSH mode).


Partial Transfers and Resume

Large file transfers interrupted mid-way can be resumed:

# Keep partial files and resume on next run
rsync -avzP --partial-dir=/tmp/rsync-partial /large-source/ user@host:/dest/

--partial-dir stores incomplete files in a separate directory rather than in the destination, preventing half-written files from being used accidentally. On the next run, rsync detects the partial file and resumes from where it stopped.


Monitoring and Logging

Per-run stats

rsync -avz --stats /source/ /dest/ 2>&1 | tee /var/log/rsync-$(date +%Y%m%d).log

Key stats output:

Number of files: 48,231
Number of created files: 12
Number of deleted files: 3
Number of regular files transferred: 47
Total file size: 18.52G
Total transferred file size: 124.73M
Transfer speed: 8.43M/s

Persistent log file

rsync -avz --log-file=/var/log/rsync-backup.log /source/ /dest/

Check for failures in scripts

rsync -avz /source/ /dest/
EXIT=$?
if [ $EXIT -ne 0 ] && [ $EXIT -ne 24 ]; then
  echo "rsync failed with exit code $EXIT" | mail -s "Backup failed on $(hostname)" admin@example.com
fi

Exit code 24 means “some source files vanished during transfer” — normal for live systems; treat as success.


rsync vs scp vs sftp vs Borg vs rclone

FeaturersyncscpsftpBorgBackuprclone
Delta transferYesNoNoYes (dedup)Partial
Resume partialYes (-P)NoYesYesYes
EncryptionVia SSHSSHSSHAES-256 nativeVia SSH/API
DeduplicationNoNoNoYesNo
Incremental snapshotsYes (--link-dest)NoNoYesNo
Cloud backendsNoNoNoSSH onlyS3, GCS, many
CompressionYes (-z)NoNolz4/zstdYes
Best forMirroring, backupsOne-off file copyInteractive filesSpace-efficient backupCloud sync

Production Backup Script

This complete script handles daily incremental backups with --link-dest, weekly and monthly rotation, and failure alerting.

#!/bin/bash
# /usr/local/bin/rsync-backup.sh
# Daily incremental backup with weekly + monthly rotation
set -euo pipefail

# ── Configuration ────────────────────────────────────────────────────────────
SOURCE_DIRS=("/home" "/etc" "/var/www")
BACKUP_HOST="backupuser@192.168.1.50"
BACKUP_ROOT="/backups/$(hostname)"
SSH_KEY="/root/.ssh/backup_ed25519"
ALERT_EMAIL="admin@example.com"
BWLIMIT=5000   # KB/s — 0 = unlimited
LOG="/var/log/rsync-backup.log"

# ── Helpers ───────────────────────────────────────────────────────────────────
TODAY=$(date +%Y-%m-%d)
DOW=$(date +%u)
DOM=$(date +%d)
RSYNC_OPTS="-aAXz --delete --stats --log-file=$LOG"
[ "$BWLIMIT" -gt 0 ] && RSYNC_OPTS="$RSYNC_OPTS --bwlimit=$BWLIMIT"
SSH_CMD="ssh -i $SSH_KEY -o StrictHostKeyChecking=yes -o BatchMode=yes"

alert() { echo "$1" | mail -s "rsync backup FAILED on $(hostname)" "$ALERT_EMAIL"; }

# ── Main backup ───────────────────────────────────────────────────────────────
LATEST="$BACKUP_HOST:$BACKUP_ROOT/daily/latest"
DEST="$BACKUP_HOST:$BACKUP_ROOT/daily/$TODAY"

for SRC in "${SOURCE_DIRS[@]}"; do
  rsync $RSYNC_OPTS \
    -e "$SSH_CMD" \
    --link-dest="$LATEST/$(basename $SRC)" \
    "$SRC/" \
    "$DEST/$(basename $SRC)/" || { alert "Failed on $SRC"; exit 1; }
done

# Update latest symlink on remote
ssh -i "$SSH_KEY" "${BACKUP_HOST%%:*}" \
  "ln -sfn $BACKUP_ROOT/daily/$TODAY $BACKUP_ROOT/daily/latest"

# ── Weekly / Monthly promotion ────────────────────────────────────────────────
ssh -i "$SSH_KEY" "${BACKUP_HOST%%:*}" bash <<EOF
  [ "$DOW" -eq 7 ] && cp -al $BACKUP_ROOT/daily/$TODAY $BACKUP_ROOT/weekly/$(date +%Y-W%V)
  [ "$DOM" -eq 01 ] && cp -al $BACKUP_ROOT/daily/$TODAY $BACKUP_ROOT/monthly/$(date +%Y-%m)
  find $BACKUP_ROOT/daily   -maxdepth 1 -mindepth 1 -type d | sort | head -n -7  | xargs -r rm -rf
  find $BACKUP_ROOT/weekly  -maxdepth 1 -mindepth 1 -type d | sort | head -n -4  | xargs -r rm -rf
  find $BACKUP_ROOT/monthly -maxdepth 1 -mindepth 1 -type d | sort | head -n -12 | xargs -r rm -rf
EOF

echo "$(date): backup complete → $TODAY" >> "$LOG"

Summary

  • rsync uses delta transfer — only changed file blocks are sent, making repeated backups fast.
  • Use -aAXz to preserve all metadata including ACLs and xattrs; add -P for resumable transfers.
  • Always dry-run with -n before adding --delete to a new destination.
  • --link-dest creates space-efficient hardlink snapshots — the foundation of professional rotation schemes.
  • Restrict backup SSH keys in authorized_keys with a forced command to limit blast radius.
  • Prefer systemd timers over cron for automated backups — better logging and missed-run handling.
  • Exit code 24 (files vanished) is normal for live systems; treat it as success in backup scripts.
  • Run rsync --stats and log output to detect silent backup failures before you need to restore.