Why rsync
rsync copies a file tree between machines (or between directories on the same machine) with minimal traffic and time. When one byte changes in a gigabyte-sized file, rsync transfers a few kilobytes of metadata, not the whole file.
Comparison:
| Tool | When to use |
|---|---|
cp -a | local copy, no network |
scp | one-off copy over SSH; does not split into blocks |
rsync | repeated synchronization; SSH or daemon |
| `tar | ssh tar` |
rclone | cloud storage (S3, GCS, Dropbox) |
Basic syntax
rsync [OPTIONS] SRC [SRC]... DEST
Local:
rsync -av /home/user/src/ /backup/src/
Over SSH (push):
rsync -av /home/user/src/ user@host:/backup/src/
Over SSH (pull):
rsync -av user@host:/var/log/ ./logs/
Trailing slash on the source: CRITICAL
/src/ - "contents of /src" -> copied into DEST
/src - "the src directory itself" -> creates DEST/src/
90% of rsync bugs come from a missing or extra slash. Always add --dry-run
before the first run.
-a: archive mode
This is shorthand for -rlptgoD:
-rrecursive-lpreserve symlinks as symlinks-ppreserve permissions-tpreserve mtime (without this, rsync sends everything every time)-gpreserve group-opreserve owner (root only)-Dspecial files (devices, sockets); rarely needed
Without -t, rsync detects changes by size. Files of equal size are skipped,
which fails silently when a single byte changes inside a large file
without altering the size. Always include -t.
Commonly used options
| Option | Effect |
|---|---|
-v | verbose; -vv more verbose; -q quiet |
-z | on-the-fly compression (useful over a network, not locally) |
-h | human-readable sizes in KiB/MiB |
-P | --partial --progress (show progress and keep partial files) |
-n | --dry-run |
--delete | remove from DEST anything absent in SRC (mirror) |
--exclude=PATTERN | skip matching files (can repeat) |
--exclude-from=FILE | read patterns from a file |
--include=PATTERN | override an exclude rule |
--bwlimit=10M | cap bandwidth |
-e ssh | specify the SSH command (port, key) |
--checksum | compare by content, not by mtime+size |
--inplace | write into existing file (for huge fixed-size files, databases) |
--link-dest=DIR | hard-link unchanged files from a previous backup (snapshot style) |
--delete, mirroring
rsync -av --delete /src/ /mirror/
After this run, /mirror/ is IDENTICAL to /src/. Files that exist in mirror
but were removed from src are deleted. Always follow this sequence:
- Run
--dry-run -v --deletefirst. - Inspect the list of deleted files carefully.
- If it looks correct, remove
-n.
This is especially dangerous in scripts that use variables:
# DANGEROUS if SRC=""
rsync -av --delete "$SRC/" /backup/ # wipes all of /backup
Exclude
rsync -av --exclude='node_modules' --exclude='.git' /src/ /backup/
rsync -av --exclude-from=.rsync-ignore /src/ /backup/
Patterns use shell-glob syntax, not regex. ** matches multiple directory levels.
A trailing / matches directories only. Full rule syntax is in man rsync
under "FILTER RULES".
SSH notes
rsync -av -e "ssh -p 2222 -i ~/.ssh/key" /src/ user@host:/dst/
If SSH prompts for a password every time, use [[ssh|ssh-agent]] or a key without a passphrase. For cron jobs, a key is required.
Speed: -z spends CPU on compression. For already-compressed data (jpg, mp4,
zip), -z hurts throughput. Turn it off.
Snapshot-style backups
The --link-dest trick:
YESTERDAY=/backup/2026-05-01
TODAY=/backup/2026-05-02
rsync -av --delete --link-dest="$YESTERDAY" /data/ "$TODAY/"
Unchanged files become hard links to yesterday's copies, using zero extra disk space.
Each 2026-MM-DD directory looks like a full copy. This is the pattern behind
tools like rsnapshot and Time Machine.
When you delete old snapshots, the hard links resolve on their own and disk space is freed naturally.
When something goes wrong
- Wrong ownership was copied: you ran rsync without root, so
-o/-ghad no effect. Use--no-perms --no-owner --no-groupor run with sudo. - rsync resends everything on every run: mtime is not preserved (missing
-t), or the destination filesystem does not support timestamps (FAT32, old SMB). Permission denied (publickey): the SSH key was not passed via-e, or the remote user lacks access.- Speed stuck at 10 MB/s on a 1 Gbit link:
-zis saturating the CPU. Remove it for already-compressed data. - Large files are interrupted when the connection drops: add
-P(--partial) so the next run resumes from where it stopped. --deleteremoved files you needed:--dry-runwas not used. On important targets, add--backup --backup-dir=....rsync error: some files/attrs were not transferred (8): usually an extended-attributes/ACL issue. Add-X -Aor exclude the affected files.
Alternatives
rclone: rsync-style CLI for S3/GCS/Dropbox/etc.bbcp,bbftp: high-throughput transfers over 10+ Gbit linksunison: bidirectional sync (rsync is one-way only)borg/restic: dedup + encryption + snapshots; rsync loses ground to these for large-volume backups