cheat sheet
rsync
Copy and synchronise files locally or over SSH using a delta-transfer algorithm that only sends changed parts. Covers archive mode, deletion, filtering, progress, and snapshot backups.
rsync — Fast Incremental File Sync
What it is
rsync is a file-copying tool originally written by Andrew Tridgell that uses a rolling-checksum delta-transfer algorithm to send only the differences between source and destination — making repeated syncs of large trees orders of magnitude faster than re-copying. It works locally, over SSH, or against a native rsync:// daemon, and preserves permissions, timestamps, ownership, symlinks, and device nodes when asked to. Reach for rsync whenever you need to keep two directory trees in sync, deploy code to a server, or perform incremental backups; for one-shot transfers of a single file from a URL, curl or wget is simpler.
Install
rsync ships in the base package set of every major distribution and on macOS, though the macOS-bundled copy is old (rsync 2.6.9 from 2006). Install a modern version (3.4.2+, protocol 32) through Homebrew on Mac to get --info=progress2, zstd compression, and the security fixes for the January 2025 CVE batch (CVE-2024-12084 through CVE-2024-12747).
# Debian/Ubuntu
sudo apt install rsync
# Fedora/RHEL
sudo dnf install rsync
# Alpine (containers)
apk add rsync
# macOS — modern build via Homebrew
brew install rsync
Output: (none — exits 0 on success)
Syntax
rsync takes one or more sources and exactly one destination. Either side may be a local path or a remote user@host:path. A trailing slash on the source means "the contents of this directory"; no trailing slash means "this directory and its contents".
rsync [OPTIONS] SRC [SRC...] DEST
rsync [OPTIONS] [user@]host:SRC DEST
rsync [OPTIONS] SRC [user@]host:DEST
Output: (none — exits 0 on success)
The trailing-slash rule
This is the single most common source of surprise: src/ (with slash) copies the contents of src into the destination; src (no slash) copies the directory itself, creating DEST/src/.
# Copies the contents of src/ into /backup/ -> /backup/file1, /backup/file2
rsync -a src/ /backup/
# Copies src as a directory into /backup/ -> /backup/src/file1, /backup/src/file2
rsync -a src /backup/
Output: (none — exits 0 on success)
Essential flags
The first flag almost every invocation needs is -a (archive mode). Add -v for verbosity, -h for human-readable sizes, and --info=progress2 for a single-line progress bar.
| Flag | Meaning |
|---|---|
-a / --archive | Archive mode — bundle of -rlptgoD (see below) |
-v | Verbose — print each file transferred |
-vv / -vvv | More verbose; useful for filter debugging |
-h | Human-readable sizes (KB, MB, GB) |
-n / --dry-run | Show what would change without doing it |
-z / --compress | Compress data in transit |
--zc=zstd | Force zstd compression (modern rsync) |
-P | Shortcut for --partial --progress |
--info=progress2 | Aggregate single-line progress for the whole transfer |
--delete | Remove files in DEST that no longer exist in SRC |
--exclude=PAT | Skip paths matching PAT |
--include=PAT | Force-include paths (use with --exclude) |
--exclude-from=FILE | Read exclude patterns from FILE |
-e ssh -p 2222 | Use a non-default SSH port |
--link-dest=DIR | Hardlink unchanged files from DIR (snapshot backups) |
--partial --append-verify | Resume an interrupted large transfer |
--bwlimit=N | Limit bandwidth to N KB/s |
--max-size=N / --min-size=N | Skip files outside size range |
Archive mode -a
-a is shorthand for -rlptgoD, which is the combination you almost always want for backups and deployments: recurse into directories, preserve symlinks, permissions, modification times, group, owner, and device/special files. It does not include -H (preserve hardlinks), -A (ACLs), or -X (xattrs) — add those explicitly if needed.
| Letter | Flag | Effect |
|---|---|---|
r | --recursive | Recurse into directories |
l | --links | Copy symlinks as symlinks |
p | --perms | Preserve permissions |
t | --times | Preserve modification times |
g | --group | Preserve group |
o | --owner | Preserve owner (root only) |
D | --devices --specials | Preserve device and special files |
# Typical archive-mode local sync
rsync -avh /home/alice/project/ /backup/project/
# Extend with hardlinks, ACLs, xattrs (full fidelity)
rsync -aHAXvh /home/alice/project/ /backup/project/
Output:
sending incremental file list
./
README.md
src/
src/main.py
tests/test_main.py
sent 8.42K bytes received 92 bytes 17.02K bytes/sec
total size is 7.81K speedup is 0.92
Dry-run first
--dry-run (or -n) shows every file that would be transferred, created, or deleted without touching the destination. Run it before any real sync, especially when --delete is in play — rsync deletions are not recoverable.
# Preview the changes
rsync -avhn --delete /home/alice/project/ /backup/project/
# Then re-run without -n once the diff looks right
rsync -avh --delete /home/alice/project/ /backup/project/
Output (--dry-run):
sending incremental file list
deleting old-feature.md
new-feature.md
docs/changelog.md
sent 156 bytes received 23 bytes 358.00 bytes/sec
total size is 7.81K speedup is 43.63 (DRY RUN)
Progress and reporting
--info=progress2 (rsync 3.1+) prints one rolling line for the entire run with total bytes, rate, and ETA — much more useful than the legacy per-file --progress. Combine with --stats to get a final summary including total file count and bytes saved by the delta algorithm.
# Modern aggregated progress
rsync -ah --info=progress2 /home/alice/photos/ /backup/photos/
# Legacy per-file progress (use -P)
rsync -avhP /home/alice/photos/ /backup/photos/
# Final statistics summary
rsync -avh --stats /home/alice/photos/ /backup/photos/
Output (--info=progress2):
1,243,668,541 87% 28.42MB/s 0:00:06 (xfr#142, to-chk=21/487)
Output (--stats):
Number of files: 487 (reg: 482, dir: 5)
Number of created files: 12
Number of regular files transferred: 12
Total file size: 1.43G bytes
Total transferred file size: 28.4M bytes
Literal data: 28.4M bytes
Matched data: 0 bytes
File list size: 14.21K
File list generation time: 0.001 seconds
Total bytes sent: 28.42M
Total bytes received: 312
sent 28.42M bytes received 312 bytes 4.04M bytes/sec
total size is 1.43G speedup is 50.32
Excluding and including
Patterns are evaluated in the order they appear; the first match wins. Use --include before a broader --exclude to "rescue" specific files. Patterns ending in / match directories only. Anchor a pattern to the transfer root with a leading /.
# Skip caches, build output, and version control
rsync -avh \
--exclude='node_modules' \
--exclude='__pycache__' \
--exclude='.git' \
--exclude='*.log' \
/home/alice/project/ /backup/project/
# Sync only .py files
rsync -avh \
--include='*/' --include='*.py' --exclude='*' \
/home/alice/project/ /backup/project/
# Patterns from a file (gitignore-like)
rsync -avh --exclude-from=/etc/rsync-excludes.txt \
/home/alice/project/ /backup/project/
Output (rescue .py files):
sending incremental file list
./
src/
src/main.py
src/utils.py
tests/
tests/test_main.py
sent 4.12K bytes received 91 bytes 8.42K bytes/sec
The
--include='*/' --include='PATTERN' --exclude='*'trio is the canonical recipe for "sync only files matching PATTERN" — you need the*/include to letrsyncdescend into directories before the per-file rules apply.
Deletion
--delete removes files in the destination that are no longer in the source — making the destination a true mirror rather than an accumulating union. The variants tweak when and how deletion happens; the default (--delete) refuses to delete if no source files were sent (which would imply a transfer error).
| Flag | Effect |
|---|---|
--delete | Delete extraneous files after the transfer |
--delete-before | Delete first, then transfer (safer if disk is tight) |
--delete-after | Delete last (safer if interrupted mid-run) |
--delete-excluded | Also delete excluded files from the destination |
--max-delete=N | Refuse the run if it would delete more than N files |
# Mirror with a sanity cap on deletions
rsync -avh --delete --max-delete=50 \
/home/alice/project/ /backup/project/
Output:
deleting old-feature.md
deleting docs/legacy/
sent 1.42K bytes received 84 bytes 3.01K bytes/sec
Over SSH
Any path of the form user@host:path is interpreted as remote and tunnelled through SSH. Pass -e 'ssh -p 2222' for a non-default port or extra SSH flags. The remote host needs rsync installed; no daemon configuration is required.
# Push to remote
rsync -avhP /home/alice/site/ alicedev@myhost:/var/www/site/
# Pull from remote
rsync -avhP alicedev@myhost:/var/log/app/ /home/alice/logs/
# Non-default SSH port + identity file
rsync -avhP -e 'ssh -p 2222 -i ~/.ssh/deploy_ed25519' \
/home/alice/site/ alicedev@myhost:/var/www/site/
# Use ProxyJump for a bastion host
rsync -avhP -e 'ssh -J alicedev@bastion.example.com' \
/home/alice/site/ alicedev@internal:/var/www/site/
Output:
alicedev@myhost's password:
sending incremental file list
./
index.html
4,231 100% 1.42MB/s 0:00:00 (xfr#1, to-chk=0/14)
sent 4.92K bytes received 87 bytes 1.43K bytes/sec
Compression in transit
-z enables compression on the wire — useful over slow links with compressible payloads (text, code). Modern rsync (3.2+) supports --zc=zstd and --zc=lz4, which are dramatically faster than zlib at similar or better ratios. Prefer --zc=zstd over plain -z whenever both sides run rsync 3.2+ — it gives better ratios and lower CPU at the same time, and rsync 3.4.2 added --compress-threads=N for multi-threaded zstd on big transfers. Skip compression entirely on a LAN: the CPU cost outweighs the savings.
# Preferred: zstd (rsync >= 3.2 both sides)
rsync -avh --zc=zstd /home/alice/logs/ alicedev@myhost:/backup/logs/
# Multi-threaded zstd (rsync >= 3.4.2)
rsync -avh --zc=zstd --compress-threads=4 \
/home/alice/logs/ alicedev@myhost:/backup/logs/
# lz4 — fastest, weakest ratio (good for already-binary blobs)
rsync -avh --zc=lz4 /home/alice/logs/ alicedev@myhost:/backup/logs/
# zlib fallback (works everywhere, including ancient peers)
rsync -avhz /home/alice/logs/ alicedev@myhost:/backup/logs/
# Tune zstd level (-131072 to 22; default ~3 is a strong balance)
rsync -avh --zc=zstd --zl=10 /home/alice/logs/ alicedev@myhost:/backup/logs/
Output: (none — exits 0 on success)
Resumable transfers
If a large transfer is interrupted, --partial keeps the half-finished file in the destination instead of deleting it, so the next run can resume. --append-verify extends an existing file and re-checksums the overlap, which is critical when the source might have changed.
# Recommended for very large files (ISOs, datasets)
rsync -avh --partial --append-verify --info=progress2 \
/home/alice/data/big.iso alicedev@myhost:/backup/
# Keep partial files in a dedicated directory
rsync -avh --partial --partial-dir=.rsync-partial \
/home/alice/data/ alicedev@myhost:/backup/
Output: (none — exits 0 on success)
Snapshot backups with --link-dest
--link-dest=DIR tells rsync that any file matching DIR is unchanged from the previous backup and should be hardlinked rather than re-copied. The result is a directory tree that looks like a full backup but only costs disk space for the files that actually changed since the reference snapshot — the foundation of Apple Time Machine, rsnapshot, and many homegrown backup scripts.
# Nightly snapshot script (idempotent)
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d 'yesterday' +%Y-%m-%d)
rsync -avh --delete \
--link-dest=/backups/snapshots/${YESTERDAY} \
/home/alice/ \
/backups/snapshots/${TODAY}/
# Verify disk savings
du -sh /backups/snapshots/${TODAY}
du -sh /backups/snapshots/${YESTERDAY}
Output:
1.4G /backups/snapshots/2026-05-24
1.4G /backups/snapshots/2026-05-23
# Real disk used (sharing hardlinks):
du -sh --apparent-size /backups/snapshots
142M /backups/snapshots
Hardlinks are per-filesystem —
--link-destonly deduplicates when SRC, DEST, and the link-dest reference are on the same partition. Across filesystems, fall back tocp -alor use--copy-dest(which still copies but reads from the reference for the delta calculation).
Bandwidth and limits
--bwlimit=N caps the average transfer rate to N kilobytes per second — necessary on shared links where rsync would otherwise saturate the connection. --max-size and --min-size skip files outside a size range, useful for excluding huge files from a quick sync.
# Cap to 5 MB/s
rsync -avhP --bwlimit=5000 \
/home/alice/data/ alicedev@myhost:/backup/
# Skip files larger than 100 MB
rsync -avh --max-size=100M /home/alice/dropbox/ /backup/dropbox/
# Skip files smaller than 1 KB
rsync -avh --min-size=1K /home/alice/dropbox/ /backup/dropbox/
Output: (none — exits 0 on success)
Configuration
For SSH-based syncs there is nothing to configure — rsync inherits credentials, port, and host keys from your SSH client. Daemon mode is the alternative transport, spoken over rsync://host/module/path or host::module/path, and it is driven entirely by rsyncd.conf — a global section followed by named module blocks that define exported paths, auth users, and access controls. Reach for it when you need anonymous public read access (mirrors), per-module ACLs, or want to avoid the per-file SSH crypto overhead on a trusted LAN. The default config path is /etc/rsyncd.conf; override with --config=FILE when running rsync --daemon for testing or non-root setups.
# /etc/rsyncd.conf — daemon-mode configuration
uid = nobody
gid = nogroup
use chroot = yes
max connections = 10
pid file = /var/run/rsyncd.pid
log file = /var/log/rsyncd.log
[mirror]
path = /srv/mirror
comment = Public read-only mirror
read only = yes
list = yes
[backups]
path = /srv/backups
comment = Authenticated backup target
read only = no
auth users = alicedev
secrets file = /etc/rsyncd.secrets
hosts allow = 10.0.0.0/24
hosts deny = *
# Start the daemon in the foreground with an alternate config (handy for testing)
rsync --daemon --no-detach --config=/etc/rsyncd.conf
# Connect using the rsync:// URL form
rsync -avh rsync://myhost/mirror/ /home/alice/mirror/
# Or the legacy double-colon form (identical semantics)
rsync -avh myhost::mirror/ /home/alice/mirror/
# Push to an authenticated module (RSYNC_PASSWORD avoids the prompt)
RSYNC_PASSWORD=hunter2 rsync -avh /home/alice/site/ \
alicedev@myhost::backups/site/
Output:
receiving incremental file list
README.md
packages/
sent 142 bytes received 8.42K bytes 17.12K bytes/sec
total size is 142.34M speedup is 16623.41
rsync daemon mode was the vector for the January 2025 CVE batch (CVE-2024-12084 heap overflow, CVE-2024-12085 info leak, plus four more). Any daemon exposed to untrusted clients must run rsync 3.4.0 or newer — ideally 3.4.2+ for the follow-up fixes. Behind SSH the daemon attack surface is not exposed, but the protocol parser is still in scope, so keep clients patched too.
Common pitfalls
- Trailing-slash confusion —
rsync -a src dstandrsync -a src/ dstproduce different layouts. Pick a convention and stick to it. --deletewithout dry-run — always preview with-nfirst; deletions are irreversible.- macOS bundled rsync is ancient — protocol 29, missing
--info=progress2. Installrsyncfrom Homebrew for any non-trivial use. --excludepatterns are not regex — they're shell-style globs.*.logmatches in any directory;/build/anchors to the transfer root.- Permission preservation needs root — without root, owner/group/SELinux contexts may silently differ. Use
--no-owner --no-groupto suppress warnings if that's intentional. - Hardlinks across filesystems silently fail —
--link-destquietly copies instead of hardlinking when partitions differ. Inspect withls -lito confirm inodes match. -zover SSH compresses twice — if your SSH config setsCompression yes, drop-zto avoid double-CPU cost.
Real-world recipes
Nightly home-directory backup
A rotating snapshot scheme that keeps the last 7 days, dedupes via hardlinks, and prunes old runs.
#!/usr/bin/env bash
set -euo pipefail
SRC=/home/alice/
DST=/backups/alice
TODAY=$(date +%Y-%m-%d)
LATEST="${DST}/latest"
mkdir -p "${DST}"
rsync -avh --delete \
--exclude='.cache' \
--exclude='node_modules' \
--link-dest="${LATEST}" \
"${SRC}" "${DST}/${TODAY}/"
ln -snf "${TODAY}" "${LATEST}"
# Keep last 7
ls -1 "${DST}" | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' \
| sort | head -n -7 \
| xargs -I{} rm -rf "${DST}/{}"
Output:
sending incremental file list
notes/
notes/2026-05-24.md
photos/IMG_2026.jpg
sent 4.21M bytes received 1.42K bytes 840.20K bytes/sec
total size is 14.21G speedup is 3372.91
Deploy a built site to a server
Push a freshly built dist/ to a web server's document root, deleting stale files and capping bandwidth so the upload doesn't starve interactive traffic.
rsync -avhP --delete \
--bwlimit=10000 \
--exclude='.well-known' \
/home/alice/site/dist/ \
alicedev@myhost:/var/www/site/
Output:
sending incremental file list
index.html
2,148 100% 1.02MB/s 0:00:00 (xfr#1, to-chk=4/14)
assets/main.css
14,231 100% 6.78MB/s 0:00:00 (xfr#2, to-chk=3/14)
deleting old-page.html
sent 18.42K bytes received 1.42K bytes 6.61K bytes/sec
Pull only the new files from a remote logs directory
Many log directories contain a mix of finished log files and currently-rotating ones; --ignore-existing skips anything the destination already has, so the sync only fetches new arrivals.
rsync -avh --ignore-existing \
alicedev@myhost:/var/log/app/ /home/alice/logs/app/
Output:
receiving incremental file list
app-2026-05-24.log
received 142.84K bytes sent 31 bytes 28.57K bytes/sec
Sync between two remotes through your laptop
Direct remote-to-remote rsync is not supported. The workaround is to stage on the local machine, or to run rsync inside an SSH session on one of the remotes.
# Option A: two-step via local
rsync -avh alicedev@host-a:/data/ /tmp/stage/
rsync -avh /tmp/stage/ alicedev@host-b:/data/
# Option B: run rsync remotely (host-a pushes to host-b)
ssh alicedev@host-a 'rsync -avh /data/ alicedev@host-b:/data/'
Output: (none — exits 0 on success)
Restore a single file from a snapshot tree
When the backup is laid out as date-stamped directories, restoring is a regular rsync (or cp) from the snapshot back to the live tree.
rsync -avh /backups/alice/2026-05-20/notes/2026-05-15.md \
/home/alice/notes/2026-05-15.md
Output:
sending incremental file list
2026-05-15.md
1,832 100% 1.42MB/s 0:00:00 (xfr#1, to-chk=0/1)
Exit codes
rsync returns a non-zero exit code on partial failures, which matters for automated backup scripts that want to distinguish "everything went fine" from "some files were skipped".
| Code | Meaning |
|---|---|
0 | Success |
1 | Syntax / usage error |
12 | Error in protocol data stream |
23 | Partial transfer (some files failed) |
24 | Partial transfer (some files vanished on sender) |
30 | Timeout |
255 | SSH connection error |
rsync -avh --dry-run --itemize-changes SRC/ DST/prints a diff-like summary (>f.st....etc.) that's easier to parse in scripts than the default verbose output. The first column of the itemize code tells you exactly what would change.
Sources
- rsync NEWS.md (upstream changelog)
- Arch Linux — Critical rsync security release 3.4.0 (Jan 2025)
- CERT/CC VU#952657 — Rsync contains six vulnerabilities
- Google Security Research — Heap Buffer Overflow, Info Leak, Server Leaks, Path Traversal and Safe links Bypass (GHSA-p5pg-x43v-mvqj)
- Arctic Wolf — Multiple Vulnerabilities in Rsync Could be Combined to Achieve RCE
- Phoronix — Rsync 3.4 Released Due To Multiple, Significant Security Vulnerabilities