opscape

Networking and remote

rsync

Copy and synchronize files and directories, locally or over ssh, transferring only what changed.

rsync [OPTION...] SOURCE... DEST     (either side may be [USER@]HOST:PATH)

12 of 12 examples run in Debian 13.7 on 2026-10-31 · rsync 3.5.0+ds1-0+deb13u1 · manual: rsync(1)

rsync makes a destination match a source. On the first run it copies everything; on later runs it compares sizes and modification times, skips files that are already identical, and for changed files sends only the parts that differ. That makes it the standard tool for backups, deployments and keeping two directories in step, whether both are local or one is on a remote host reached over ssh (host:path).

Nearly every invocation starts with -a (archive): recurse, and keep permissions, timestamps, symlinks, owners (when run as root) and device files. Add -v to see the file list, -n to rehearse, --delete to remove files that no longer exist in the source, and -z to compress over slow links.

The one rule to memorize is the trailing slash on the source: rsync -a src/ dest/ copies the contents of src into dest, while rsync -a src dest/ copies the directory itself, producing dest/src/. The slash on the destination does not matter.

In these examples the remote host server is the sandbox itself, reached over ssh, so server:backup/ lands in /home/dev/backup.

Examples

Copy a directory, keeping its attributes

rsync -av src/ backup/
explain
output
sending incremental file list
created directory backup
./
Makefile
app.py
main.c
util.sh
lib/
lib/helpers.py

sent 1,151 bytes  received 155 bytes  2,612.00 bytes/sec
total size is 745  speedup is 0.57

-a copies recursively and preserves modes and times; -v lists each file. The summary shows bytes sent and received. speedup below 1 is normal on a first copy, which has to send everything plus the file list.

A second run copies nothing

Setup run before this example
rsync -a src/ backup/
echo '// tweak' >> src/main.c
rsync -av src/ backup/
output
sending incremental file list
main.c

sent 317 bytes  received 36 bytes  706.00 bytes/sec
total size is 754  speedup is 2.14

backup/ is already up to date except for the one file just changed, so only main.c is transferred. Unchanged files are skipped by comparing size and modification time, which is why -a (keeping times) matters for the next run.

The trailing slash on the source

rsync -a src with-dir/
rsync -a src/ contents-only/
find with-dir contents-only -maxdepth 2 | sort
output
contents-only
contents-only/Makefile
contents-only/app.py
contents-only/lib
contents-only/lib/helpers.py
contents-only/main.c
contents-only/util.sh
with-dir
with-dir/src
with-dir/src/Makefile
with-dir/src/app.py
with-dir/src/lib
with-dir/src/main.c
with-dir/src/util.sh

Without a slash, src itself is copied, giving with-dir/src/.... With src/, its contents go straight into contents-only/. The destination's trailing slash makes no difference.

Rehearse a mirror with --delete and -n

Setup run before this example
rsync -a src/ mirror/
touch mirror/stale.txt
echo '// tweak' >> src/main.c
rsync -avn --delete src/ mirror/
explain
output
sending incremental file list
deleting stale.txt
./
main.c

sent 185 bytes  received 39 bytes  448.00 bytes/sec
total size is 754  speedup is 3.37 (DRY RUN)

--delete removes files in the destination that are not in the source; -n (--dry-run) only reports what would happen. stale.txt would be deleted and main.c updated, and the summary ends in (DRY RUN). Nothing has changed yet.

Mirror for real and see what changed

Setup run before this example
rsync -a src/ mirror/
touch mirror/stale.txt
echo '// tweak' >> src/main.c
rsync -ai --delete src/ mirror/
ls mirror
output
*deleting   stale.txt
.d..t...... ./
>f.st...... main.c
Makefile
app.py
lib
main.c
util.sh

-i (itemize changes) prints a code per change: *deleting for removals, and for >f.st...... a file (f) received (>) because its size (s) and time (t) differ. .d..t...... means only the directory's timestamp was updated.

Leave files out with --exclude

rsync -av --exclude='*.py' --exclude=lib/ src/ backup/
explain
output
sending incremental file list
created directory backup
./
Makefile
main.c
util.sh

sent 532 bytes  received 105 bytes  1,274.00 bytes/sec
total size is 294  speedup is 0.46

Patterns are matched against paths relative to the source. A trailing slash (lib/) matches only directories. Excluded files are also protected from --delete on the receiving side.

Copy to a remote host over ssh

rsync -av data logs server:backup/
ssh server 'ls backup'
output
sending incremental file list
created directory backup
data/
data/config.json
data/fruits.txt
data/hosts.txt
data/inventory.tsv
data/numbers.txt
data/poem.txt
data/sales.csv
data/servers.json
data/settings.conf
data/users.csv
data/windows.txt
logs/
logs/app.log
logs/app.log.1
logs/error.log

sent 4,028 bytes  received 319 bytes  2,898.00 bytes/sec
total size is 3,043  speedup is 0.70
data
logs

server:backup/ means the backup directory in your home on the remote host; rsync starts itself there over ssh and creates the last path component if missing. Because data and logs have no trailing slash, they arrive as directories.

Compress and show progress with -z and -P

rsync -azP logs/ server:backup-logs/
explain
output
sending incremental file list
./
app.log

            729 100%    0.00kB/s    0:00:00  
            729 100%    0.00kB/s    0:00:00 (xfr#1, to-chk=2/4)
app.log.1

            150 100%  146.48kB/s    0:00:00  
            150 100%  146.48kB/s    0:00:00 (xfr#2, to-chk=1/4)
error.log

            112 100%  109.38kB/s    0:00:00  
            112 100%  109.38kB/s    0:00:00 (xfr#3, to-chk=0/4)

-z compresses data in transit, which helps on slow links and wastes CPU on fast ones. -P combines --progress (a per-file progress line) and --partial (keep half-sent files so an interrupted transfer can resume).

Pull from a remote host

Setup run before this example
ssh server 'mkdir -p reports && cp demo/data/sales.csv demo/data/users.csv reports/'
rsync -av server:reports/ ./reports/
ls reports
output
receiving incremental file list
created directory ./reports
./
sales.csv
users.csv

sent 65 bytes  received 1,029 bytes  2,188.00 bytes/sec
total size is 841  speedup is 0.77
sales.csv
users.csv

The remote side can be the source just as well. With the trailing slash, the files inside reports/ on the remote host land directly in the local reports/.

Summarize a transfer with --stats

rsync -ah --stats data/ backup/ | head -n 9
explain
output

Number of files: 12 (reg: 11, dir: 1)
Number of created files: 12 (reg: 11, dir: 1)
Number of deleted files: 0
Number of regular files transferred: 11
Total file size: 2.05K bytes
Total transferred file size: 2.05K bytes
Literal data: 2.05K bytes
Matched data: 0 bytes

--stats prints counts of created, deleted and transferred files and the bytes moved; -h makes the sizes human-readable. head keeps the most useful lines.

Missing parent directories on the destination

rsync -a data/ server:archive/2026/data/
rsync -a --mkpath data/ server:archive/2026/data/ && echo created
output
rsync: [Receiver] mkdir "/home/dev/archive/2026/data" failed: No such file or directory (2)
rsync error: error in file IO (code 11) at main.c(808) [Receiver=3.5.0]
created

rsync creates only the last directory of the destination. The first command fails because archive/2026 does not exist (exit code 11). --mkpath (rsync 3.2.3 and later) creates the whole path.

A source that does not exist

rsync -a nope/ backup/
explain
outputexit 23
rsync: [sender] change_dir "/home/dev/demo/nope" failed: No such file or directory (2)
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1394) [sender=3.5.0]

rsync reports the missing directory, adds some files/attrs were not transferred and exits with 23. In scripts, treat any non-zero status from rsync as a failed backup.

Common pitfalls

  • rsync -a src dest and rsync -a src/ dest differ: the first creates dest/src. Decide before you add --delete, or a mistaken slash can delete a whole tree on the destination.

  • Always run --delete with -n first and read the deleting lines.

  • Without -a (or -t), times are not preserved, so the next run cannot skip unchanged files and copies everything again.

  • rsync needs to be installed on both ends for remote copies. The error rsync: command not found from the remote side means it is missing there.

  • Owners and groups are only preserved when the receiving side runs as root; otherwise files belong to the user who received them.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • macOS shipped the old rsync 2.6.9 for many years, and recent macOS versions use openrsync, a separate implementation. Neither supports every option of current rsync (--mkpath, for instance, arrived in 3.2.3); check rsync --version and install a current rsync from Homebrew if you need them.

Options worth knowing

A selection, described in our own words. For the full list, see rsync(1) or rsync --help.

OptionWhat it does
-a, --archiveArchive mode: recurse and preserve permissions, times, symlinks, owner, group and devices (-rlptgoD).
-v, --verboseList transferred files; repeat for more detail.
-n, --dry-runShow what would be done without changing anything.
-r, --recursiveRecurse into directories.
-z, --compressCompress data during the transfer.
-PSame as --partial --progress.
--progressShow progress for each file.
--partialKeep partially transferred files so the transfer can resume.
-h, --human-readablePrint sizes in human-readable units.
-i, --itemize-changesPrint a change code for every updated item.
--deleteDelete destination files that do not exist in the source.
--exclude PATTERNSkip files matching PATTERN.
--include PATTERNDo not exclude files matching PATTERN (order matters).
--exclude-from FILERead exclude patterns from FILE.
--statsPrint transfer statistics at the end.
-e, --rsh COMMANDRemote shell to use, for example 'ssh -p 2222'.
-u, --updateSkip files that are newer on the receiving side.
-c, --checksumCompare files by checksum instead of size and time.
-l, --linksCopy symlinks as symlinks.
-L, --copy-linksCopy the files symlinks point to.
-H, --hard-linksPreserve hard links.
-A, --aclsPreserve ACLs.
-X, --xattrsPreserve extended attributes.
-t, --timesPreserve modification times.
-p, --permsPreserve permissions.
--mkpathCreate missing parent directories of the destination.
--bwlimit RATELimit bandwidth, in KiB per second unless a suffix is given.
--remove-source-filesDelete source files after they are transferred (not directories).
--backup-dir DIRWith -b, put replaced and deleted files in DIR.
-b, --backupKeep a backup of files that are replaced or deleted.
--info FLAGSFine-grained output control, for example --info=progress2 for one overall progress line.

Related