The six rsync flags worth knowing, and the one that builds you free snapshots
Published by RodHat

Everyone types rsync -avz. Most people can’t say what the a covers, and it does not cover as much as they assume — which is how a “full backup” ends up missing the ACLs that made the permissions work.
What -a actually is
-a is shorthand for -rlptgoD: recurse, copy symlinks as symlinks, preserve permissions, times, group, owner, and device/special files.
Note what’s not in there:
-Hhard links. Without it, a tree with hard-linked files becomes a tree with N independent copies. Your backup gets bigger and the link relationship — which some software depends on — is gone.-AACLs. POSIX ACLs are silently dropped. If you’ve done any fine-grained permission work,-athrows it away and the restore comes back with permissions that look right inls -land don’t work.-Xextended attributes. SELinux contexts, macOS metadata, capabilities set withsetcap. All dropped. Restore a binary that hadcap_net_bind_serviceand it can no longer bind port 443, and nothing in the output mentioned it.-Ssparse files. A sparse 200GB VM image is copied as a real 200GB file. On the wire and on the disk.
So the flag set for anything you intend to restore from is:
rsync -aHAXS --numeric-ids src/ dst/
--numeric-ids matters when source and destination don’t share a user database — a container, a rescue system, a different distro. Without it, rsync maps by name, and if the name doesn’t exist on the target, ownership silently lands on whatever it resolved to. Restoring a system backup without --numeric-ids is how you get a root filesystem owned by a user that happens to have the same name as a service account.
The trailing slash
rsync -a /src/ /dst/ # contents of /src into /dst
rsync -a /src /dst/ # /src itself into /dst -> /dst/src
A trailing slash on the source means “the contents of.” Nobody has ever gotten this right first try, including me. Run it with -n (--dry-run) plus -i (itemize) and look before you commit:
rsync -aHAXSn -i /src/ /dst/ | head -20
The itemize output is worth learning — >f+++++++++ is a new file, >f..t...... is content identical but timestamp differs, *deleting is what --delete is about to remove. Reading that for ten seconds prevents most rsync accidents.
—inplace vs —partial
These get combined by people who think they’re related. They solve different problems and combining them is sometimes wrong.
--partial keeps a partially-transferred file when the transfer dies, so the retry resumes instead of restarting. Default behavior writes to a temp name and renames on completion, so an interrupted transfer leaves nothing behind and you start over. On a 400GB file over a flaky link, --partial is the difference between finishing and not. Use --partial-dir=.rsync-partial so the fragments land somewhere identifiable instead of alongside the real files.
--inplace writes directly into the destination file rather than to a temp copy. Two reasons to want it: the destination is huge and you don’t have room for a second copy, or the destination’s storage does copy-on-write/dedup and the rename would break the sharing. It’s also the right choice for VM images and database files where only parts change.
The cost of --inplace is real and worth stating plainly: the destination file is inconsistent while the transfer runs, and if it dies you have a corrupted file with no original. The temp-file-and-rename dance exists to make updates atomic. --inplace gives that up. Never use it on the only copy of something you care about, and never on a live file another process is reading.
—link-dest, the good one
This is the flag that makes rsync a backup system.
rsync -aHAXS --delete \
--link-dest=/backups/2026-07-29 \
/data/ /backups/2026-07-30/
For every file that is unchanged from the --link-dest directory, rsync creates a hard link instead of copying the data. Changed files get copied fresh.
So /backups/2026-07-30/ is a complete, browsable, restore-from-directly full snapshot of /data — and it costs you only the blocks that changed since yesterday. Ten daily snapshots of a 500GB tree with 2GB of daily churn take about 520GB, not 5TB. Delete any one of them and the others are unaffected, because they’re hard links and the data survives until the last link goes.
No proprietary format, no catalog database, no restore tool. cp is the restore tool. That property — that your backups are just directories — is worth an enormous amount at 3am, and it’s worth more than whatever feature the backup product with the dashboard is selling.
The wrapper is about ten lines:
#!/bin/sh
set -eu
DEST=/backups/$(date +%F)
LAST=$(ls -1d /backups/20* 2>/dev/null | tail -1)
rsync -aHAXS --delete --numeric-ids \
${LAST:+--link-dest="$LAST"} \
/data/ "$DEST.incomplete/"
mv "$DEST.incomplete" "$DEST"
The .incomplete rename is the important line. A snapshot directory only gets its real name once rsync exited zero, so a half-finished backup can never be mistaken for a good one — by you, or by tomorrow’s --link-dest. That mistake compounds: link against a truncated snapshot and every subsequent backup inherits the damage.
And on a filesystem with real snapshots, use those instead — zfs snapshot does this better and instantly. --link-dest is for when the destination is ext4 on somebody else’s hardware, which is most of the time.
The rest
-z compresses on the wire. Pointless for already-compressed data and it costs CPU; on a fast LAN it’s usually a net loss. --bwlimit=10m when you’re saturating a link somebody else needs. --checksum when timestamps lie to you, and only then — it reads every byte on both ends. --delete deletes; combine it with -n the first time, every time, forever.