rsync copies files, but only the parts that changed. On the second run of a large transfer it sends almost nothing, which makes it the right tool for anything you will do more than once — deployments, backups, moving a home directory between machines.
It also has one piece of syntax that behaves differently from every other copy command, and gets people every time. That is the first thing to learn.
The trailing slash
rsync -a source/ /backup/ # copies the CONTENTS of source into /backup/
rsync -a source /backup/ # copies the DIRECTORY into /backup/source/A trailing slash on the source means “the things inside this directory”. No trailing slash means “this directory itself”. The destination’s slash makes no difference.
Get it wrong and you end up with /backup/source/source/, or, worse, files landing directly in a destination you meant to keep tidy. When in doubt, run it with --dry-run and read the first few paths.
The flags that matter
rsync -avh --progress source/ dest/| Flag | What it does |
|---|---|
-a | Archive: recursive, and preserves almost everything (see below) |
-v | Verbose — list what is transferred |
-h | Human-readable sizes |
-z | Compress during transfer — for slow links only |
-P | --partial --progress: show progress, keep partial files for resuming |
-n | Dry run — change nothing, show what would happen |
--delete | Remove files from the destination that are gone from the source |
--exclude | Skip matching paths |
-H | Preserve hard links — not included in -a |
-x | Stay on one filesystem |
-c | Compare by checksum rather than size and time |
-a is shorthand for -rlptgoD: recursive, symlinks preserved as symlinks, permissions, times, group, owner, and device files. It is what you want almost always — but note that preserving owner and group needs root, and that -a does not include -H. If the source uses hard links (see ln), a plain -a copy silently expands them into separate full-size files.
Over SSH
rsync -avh source/ user@server:/opt/dest/ # push
rsync -avh user@server:/var/log/ ./logs/ # pull
rsync -avh -e "ssh -p 2222" source/ server:/dest/ # non-standard port
rsync -avhP --bwlimit=5000 big/ server:/dest/ # cap at ~5 MB/srsync uses SSH by default, so anything defined in ~/.ssh/config — hostnames, keys, ports, ProxyJump — applies automatically. Setting that up once, as covered in the ssh page, removes the need for -e entirely.
Note that -z is not free: it costs CPU on both ends. Over a fast local network it usually makes the transfer slower, and it achieves nothing on data that is already compressed — images, video, or tarballs.
–delete, and why to fear it
--delete makes the destination match the source exactly, which means removing anything the source does not have. It is what you want for a mirror, and it is the single most destructive thing rsync can do.
# ALWAYS do this first
rsync -avh --delete --dry-run source/ dest/
# Then, once you have read the output
rsync -avh --delete source/ dest/The specific danger is combining --delete with a mistyped or empty source. If the source path does not exist as you expect, rsync happily concludes the destination should also contain nothing. Two safeguards worth using:
rsync -avh --delete --max-delete=100 source/ dest/ # abort if it wants to delete more than 100
rsync -avh --delete --dry-run source/ dest/ | grep '^deleting' | wc -l--max-delete in particular turns a catastrophe into an error message.
Excluding things
rsync -avh --exclude='.git' --exclude='node_modules' --exclude='*.log' \
project/ server:/opt/project/
# From a file, one pattern per line
rsync -avh --exclude-from='exclude.txt' project/ server:/opt/project/
# Exclude everything except one type
rsync -avh --include='*/' --include='*.jpg' --exclude='*' source/ dest/Patterns are matched against the path relative to the transfer root. A leading slash anchors to that root: --exclude='/cache' excludes only the top-level cache, while --exclude='cache' excludes any directory of that name at any depth.
Order matters for include/exclude — the first matching rule wins, which is why that last example needs --include='*/' to descend into directories before the catch-all exclude applies.
Snapshot backups
This is rsync’s best trick, and it is why -H and hard links matter.
DATE=$(date +%F)
rsync -avh --delete \
--link-dest=/backup/latest \
/home/kevin/ /backup/$DATE/
rm -f /backup/latest
ln -s /backup/$DATE /backup/latest--link-dest tells rsync that where a file is unchanged from the previous backup, it should create a hard link to the existing copy rather than transferring it again. The result is a directory per day, each of which looks like a complete backup, while unchanged files exist only once on disk.
Thirty daily snapshots of a mostly-static home directory can cost barely more than one copy. Deleting any one snapshot is safe — the data survives while any other snapshot still links to it. This is the mechanism behind Time Machine and most rsync-based backup tools.
Schedule it with cron or a systemd timer — and log the output somewhere, because a backup that has been silently failing is worse than no backup at all.
Gotchas
Nothing transfers on the second run
That is correct behaviour. By default rsync skips files whose size and modification time both match. If you genuinely suspect corruption or a mangled timestamp, -c forces a checksum comparison — much slower, but definitive.
Permissions and ownership need root
As a normal user, -a cannot set arbitrary owners, so everything arrives owned by you and rsync may warn about it. For a faithful copy you need root at the receiving end: --rsync-path="sudo rsync", which requires the remote account to have the appropriate sudo rights.
Interrupted transfers
Without -P, a partially transferred file is discarded and restarted from scratch. With it, the partial file is kept and resumed. On anything large or over an unreliable link, -P should be habitual.
Dry run does not catch everything
--dry-run shows which files would move, but it cannot always predict disk-space failures or permission errors at the destination. It is a very good check, not a guarantee.
rsync is not versioning
A plain rsync --delete to a backup destination faithfully replicates your mistakes: delete something by accident and the next run removes it from the backup too. Snapshots with --link-dest fix that; so do restic and borg, which add deduplication, encryption and integrity checking. rsync is a superb transfer tool and only half a backup strategy.
Quick reference
rsync -avh src/ dst/ # contents of src into dst
rsync -avh src dst/ # src itself into dst/src/
rsync -avhn --delete src/ dst/ # DRY RUN first, every time
rsync -avh --delete --max-delete=100 src/ dst/
rsync -avhP src/ user@host:/dst/ # over ssh, resumable, with progress
rsync -avh -e "ssh -p 2222" src/ host:/dst/
rsync -avh --bwlimit=5000 src/ host:/dst/
rsync -avh --exclude='.git' --exclude='node_modules' src/ dst/
rsync -avh --exclude-from=exclude.txt src/ dst/
rsync -avh --delete --link-dest=/backup/latest src/ /backup/$(date +%F)/
rsync -avhH src/ dst/ # preserve hard links (-a does not)
rsync -avhc src/ dst/ # compare by checksumRelated
- ssh — the transport, and the config file that makes rsync commands short.
- ln — the hard links that make
--link-destsnapshots so cheap. - tar — for packaging a directory rather than syncing it.
- cron — scheduling the backup, and logging it so failures are visible.
- Disk space — snapshots make
dufigures counterintuitive. restic,borg— when you want real backups with deduplication and encryption.
