What rsync does differently from scp/cp¶
rsync copies files, but unlike scp or cp, it only transfers the parts of a file that have actually changed (using a rolling-checksum delta algorithm), and it can skip files entirely if the destination already matches. This makes it dramatically faster than scp for repeated syncs of mostly-unchanged data — a nightly backup of a large directory tree only pays the transfer cost for what actually changed that day.
Running rsync over SSH just means it uses SSH as the transport for talking to a remote host — no separate rsync daemon needs to be running on the other end, and the traffic is encrypted the same as any other SSH session. This is by far the most common way to use rsync remotely (the alternative, an rsync:// daemon, is mostly seen in mirror/package-repository contexts).
Basic syntax¶
rsync [options] source destination
Either side can be local or remote. A remote path uses the same user@host:path syntax as scp:
# local → remote
rsync -avz /local/dir/ user@remote-host:/remote/dir/
# remote → local
rsync -avz user@remote-host:/remote/dir/ /local/dir/
The trailing-slash gotcha¶
This trips up nearly everyone at least once: whether the source path ends in / changes what gets copied.
rsync -av /local/dir user@host:/remote/dir/ # copies "dir" itself into /remote/dir/
rsync -av /local/dir/ user@host:/remote/dir/ # copies the *contents* of dir into /remote/dir/
/local/dir(no trailing slash) → creates/remote/dir/dir/...on the destination./local/dir/(trailing slash) → copies everything insidedirdirectly into/remote/dir/..., without an extra nested folder.
The destination path's trailing slash doesn't matter the same way — it's specifically the source slash that controls this behavior. When in doubt, --dry-run (below) shows you exactly what would happen before it happens.
Common flags¶
| Flag | Meaning |
|---|---|
-a |
Archive mode — shorthand for -rlptgoD (recursive, preserve symlinks, permissions, times, group, owner, and device/special files). This is the flag you want for almost any real sync. |
-v |
Verbose — list files as they're transferred. |
-z |
Compress data during transfer (helps over slow links, usually not worth it on a fast LAN). |
-P |
Shorthand for --progress --partial — shows a per-file progress bar and keeps partially-transferred files so an interrupted transfer can resume. |
-n / --dry-run |
Show what would happen without actually copying anything. Always worth running once with a new/unfamiliar command before removing -n. |
--delete |
Delete files on the destination that no longer exist on the source — makes the destination a true mirror rather than an additive copy. Dangerous without --dry-run first. |
-e ssh |
Explicitly specify SSH as the remote shell (usually implied automatically when you use user@host:path syntax, but needed if you want to pass SSH options — see below). |
--exclude=PATTERN |
Skip files/dirs matching a pattern (can be repeated). |
--exclude-from=FILE |
Read exclude patterns from a file, one per line. |
-u / --update |
Skip files on the destination that are newer than the source's version. |
--bwlimit=RATE |
Cap transfer bandwidth (e.g. --bwlimit=5000 for ~5MB/s), to avoid saturating a shared link. |
--stats |
Print a summary of what was transferred at the end. |
A typical everyday command:
rsync -avzP --exclude='.git' --exclude='node_modules' ./project/ user@host:/srv/project/
Customizing the SSH connection¶
If you need a non-default SSH port, a specific key, or any other SSH option, pass it via -e:
rsync -avz -e "ssh -p 2222 -i ~/.ssh/id_ed25519_deploy" /local/dir/ user@host:/remote/dir/
If the host is already configured in ~/.ssh/config (with its HostName, Port, IdentityFile, etc.), you don't need -e at all — just reference the config's Host alias:
rsync -avz /local/dir/ myhost:/remote/dir/
Excluding files¶
Inline, repeatable per pattern:
rsync -avz --exclude='*.log' --exclude='.cache/' /local/dir/ user@host:/remote/dir/
Or from a file (one pattern per line, same syntax as .gitignore-style globs, though not identical in every edge case):
rsync -avz --exclude-from='exclude-patterns.txt' /local/dir/ user@host:/remote/dir/
A pattern ending in / matches directories only; a leading / anchors the pattern to the root of the transfer rather than matching at any depth.
Mirroring vs. additive sync¶
By default, rsync only adds/updates files at the destination — it never removes anything, even if a source file was deleted. To make the destination an exact mirror of the source (removing destination files that no longer exist on the source), add --delete:
rsync -avz --delete /local/dir/ user@host:/remote/dir/
Always test this with --dry-run first, especially the first time you run a --delete sync against a destination you care about:
rsync -avzn --delete /local/dir/ user@host:/remote/dir/
-n (dry-run) combined with -v shows exactly which files would be deleted without touching anything — a deleting <path> line in the output for each one.
Resuming interrupted transfers¶
rsync -avzP --partial /local/dir/ user@host:/remote/dir/
--partial keeps partially-transferred files instead of deleting them on interruption, so a re-run of the same command picks up from where it left off rather than re-transferring the whole file. -P already includes --partial (plus --progress), so you rarely need to specify --partial separately once you're using -P.
For very unreliable connections, --partial-dir=.rsync-partial keeps in-progress files in a hidden subdirectory instead of alongside the real files, so a half-finished file is never mistaken for a complete one if something else reads that directory mid-transfer.
Incremental backups with --link-dest¶
A classic rsync pattern for space-efficient dated backups: each backup run only stores files that changed since the previous backup, but unchanged files are hard-linked to the previous backup rather than re-copied — so every dated backup folder looks like a complete, independent snapshot, but only changed files actually consume new disk space.
rsync -a --delete \
--link-dest=/backups/2026-09-15 \
/local/dir/ \
/backups/2026-09-16/
--link-dest points at the previous backup directory. Any file unchanged since then is hard-linked instead of copied; anything new or modified is copied normally. This only works when source and destination are on the same filesystem as the link-dest target (hard links can't span filesystems) — for a remote destination, --link-dest needs to reference a path on the remote side, not the local one.
Checking what actually differs (checksum vs. quick-check)¶
By default, rsync decides whether a file needs transferring based on size and modification time (fast, but can theoretically miss a change if both happen to match). For a more thorough — but much slower — comparison based on actual file content:
rsync -avzc /local/dir/ user@host:/remote/dir/
-c/--checksum forces rsync to checksum file contents on both ends rather than trusting size+mtime. Worth using after something like a clock skew issue or a restore from a backup where mtimes might not be trustworthy — not something to leave on by default, since it makes every sync noticeably slower on large trees.
Troubleshooting¶
"Permission denied" on the remote side — check the remote user actually owns/can write to the destination path; rsync over SSH runs as whatever user you connected as, same as any other SSH command.
rsync: connection unexpectedly closed — usually either an SSH-level failure (wrong key, wrong port, host key mismatch) rather than an rsync problem specifically. Test the plain SSH connection first (ssh user@host) before assuming rsync itself is broken.
Protocol version mismatch between rsync versions — very old and very new rsync versions occasionally negotiate incompatible protocol versions. If you hit this and can't upgrade one side, --protocol=NN can force an older, compatible protocol version explicitly.
Sync seems to skip files that clearly changed — check for clock skew between hosts (rsync's fast-path change detection relies on modification times) or add -c (see above) to force a content-based check instead of trusting timestamps.
Slow performance on many small files — rsync's per-file overhead adds up on trees with huge numbers of tiny files; consider whether the data could be tarred first and transferred as one stream if it's a one-time migration rather than an ongoing sync (rsync's incremental-update benefit only matters for repeated syncs of the same tree).
Quick reference¶
| Task | Command |
|---|---|
| Basic sync | rsync -avz src/ user@host:dst/ |
| Preview before deleting | rsync -avzn --delete src/ user@host:dst/ |
| Mirror exactly | rsync -avz --delete src/ user@host:dst/ |
| Resumable, with progress | rsync -avzP src/ user@host:dst/ |
| Custom SSH port/key | rsync -avz -e "ssh -p 2222 -i keyfile" src/ user@host:dst/ |
| Exclude patterns | rsync -avz --exclude='*.log' src/ user@host:dst/ |
| Content-based comparison | rsync -avzc src/ user@host:dst/ |
| Space-efficient dated backup | rsync -a --delete --link-dest=../prev src/ ./today/ |
Further reading¶
man rsync— the flag reference above only scratches the surface; rsync has dozens more options for filtering, timeouts, and remote shell tuning.- https://rsync.samba.org/documentation.html