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).

bash
# 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".

bash
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/.

bash
# 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.

FlagMeaning
-a / --archiveArchive mode — bundle of -rlptgoD (see below)
-vVerbose — print each file transferred
-vv / -vvvMore verbose; useful for filter debugging
-hHuman-readable sizes (KB, MB, GB)
-n / --dry-runShow what would change without doing it
-z / --compressCompress data in transit
--zc=zstdForce zstd compression (modern rsync)
-PShortcut for --partial --progress
--info=progress2Aggregate single-line progress for the whole transfer
--deleteRemove files in DEST that no longer exist in SRC
--exclude=PATSkip paths matching PAT
--include=PATForce-include paths (use with --exclude)
--exclude-from=FILERead exclude patterns from FILE
-e ssh -p 2222Use a non-default SSH port
--link-dest=DIRHardlink unchanged files from DIR (snapshot backups)
--partial --append-verifyResume an interrupted large transfer
--bwlimit=NLimit bandwidth to N KB/s
--max-size=N / --min-size=NSkip 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.

LetterFlagEffect
r--recursiveRecurse into directories
l--linksCopy symlinks as symlinks
p--permsPreserve permissions
t--timesPreserve modification times
g--groupPreserve group
o--ownerPreserve owner (root only)
D--devices --specialsPreserve device and special files
bash
# 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:

text
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.

bash
# 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):

text
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.

bash
# 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):

text
        1,243,668,541  87%   28.42MB/s    0:00:06 (xfr#142, to-chk=21/487)

Output (--stats):

text
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 /.

bash
# 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):

text
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 let rsync descend 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).

FlagEffect
--deleteDelete extraneous files after the transfer
--delete-beforeDelete first, then transfer (safer if disk is tight)
--delete-afterDelete last (safer if interrupted mid-run)
--delete-excludedAlso delete excluded files from the destination
--max-delete=NRefuse the run if it would delete more than N files
bash
# Mirror with a sanity cap on deletions
rsync -avh --delete --max-delete=50 \
  /home/alice/project/ /backup/project/

Output:

text
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.

bash
# 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:

text
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.

bash
# 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.

bash
# 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)

--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.

bash
# 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:

text
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-dest only deduplicates when SRC, DEST, and the link-dest reference are on the same partition. Across filesystems, fall back to cp -al or 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.

bash
# 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.

ini
# /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 = *
bash
# 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:

text
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

  1. Trailing-slash confusionrsync -a src dst and rsync -a src/ dst produce different layouts. Pick a convention and stick to it.
  2. --delete without dry-run — always preview with -n first; deletions are irreversible.
  3. macOS bundled rsync is ancient — protocol 29, missing --info=progress2. Install rsync from Homebrew for any non-trivial use.
  4. --exclude patterns are not regex — they're shell-style globs. *.log matches in any directory; /build/ anchors to the transfer root.
  5. Permission preservation needs root — without root, owner/group/SELinux contexts may silently differ. Use --no-owner --no-group to suppress warnings if that's intentional.
  6. Hardlinks across filesystems silently fail--link-dest quietly copies instead of hardlinking when partitions differ. Inspect with ls -li to confirm inodes match.
  7. -z over SSH compresses twice — if your SSH config sets Compression yes, drop -z to 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.

bash
#!/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:

text
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.

bash
rsync -avhP --delete \
  --bwlimit=10000 \
  --exclude='.well-known' \
  /home/alice/site/dist/ \
  alicedev@myhost:/var/www/site/

Output:

text
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.

bash
rsync -avh --ignore-existing \
  alicedev@myhost:/var/log/app/ /home/alice/logs/app/

Output:

text
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.

bash
# 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.

bash
rsync -avh /backups/alice/2026-05-20/notes/2026-05-15.md \
  /home/alice/notes/2026-05-15.md

Output:

text
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".

CodeMeaning
0Success
1Syntax / usage error
12Error in protocol data stream
23Partial transfer (some files failed)
24Partial transfer (some files vanished on sender)
30Timeout
255SSH 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