SCP & Rsync Cheatsheet - File Transfer & Remote Copy
Two commands, one job, and the difference decides how much time and bandwidth you lose when the link wobbles. scp always ships the whole file: change one byte and every byte is retransmitted, and an interrupted transfer has no resume point. On a LAN that costs nothing; over a WAN with a 2 GB artifact it can cost you the download twice. rsync computes a rolling checksum, transfers only changed blocks, and approximates resume when --partial keeps the partial file and --append-verify grows it under verification. The trailing slash is the other difference that quietly decides whether you end up with an extra nesting level: src/ syncs the contents of a directory, src syncs the directory itself. The decision rule is short: a one-off single file is faster with scp because the command is short and macOS ships it; anything recurring — directory sync, deploys, scheduled backups — is rsync. In both cases adding -o ServerAliveInterval stops middleboxes from silently killing a long transfer.
Typical Use Case
For engineers who move bytes between hosts, keep long-running jobs alive across SSH drops, or ship local changes to servers every day. This sheet covers scp and rsync side by side, because the two look interchangeable and are not: scp copies, rsync reconciles. Use it to decide which tool a transfer deserves, to pick the flags that survive interruption, and to avoid the mirror-delete and trailing-slash surprises that only show up on the second run.
scp Basics 8
scp file user@host:/pathscp user@host:/path/file .scp -r dir user@host:/pathscp -P 2222 file user@host:/pathscp -i ~/.ssh/id_rsa file user@host:/pathscp -C file user@host:/pathscp -q file user@host:/pathscp -v file user@host:/pathrsync Basics 8
rsync file user@host:/pathrsync user@host:/path/file .rsync -r dir user@host:/pathrsync -a dir user@host:/pathrsync -av dir user@host:/pathrsync -az dir user@host:/pathrsync -e "ssh -p 2222" file user@host:/pathrsync -e "ssh -i key" file user@host:/pathrsync Incremental 6
rsync -av --delete src/ user@host:/path/rsync -av --update src/ user@host:/path/rsync -av --ignore-existing src/ user@host:/path/rsync -av --dry-run src/ user@host:/path/rsync -av --progress src/ user@host:/path/rsync -av --partial src/ user@host:/path/rsync Filtering 5
rsync -av --include="*.txt" src/ dest/rsync -av --exclude="*.tmp" src/ dest/rsync -av --exclude=".git" --exclude="node_modules" src/ dest/rsync -av --exclude-from=exclude.txt src/ dest/rsync -av --filter="+ *.txt" --filter="- *" src/ dest/rsync Advanced 6
rsync -av --compress src/ dest/rsync -av --checksum src/ dest/rsync -av --size-only src/ dest/rsync -av --modify-window=1 src/ dest/rsync -av --log-file=rsync.log src/ dest/rsync -av --stats src/ dest/Common Patterns 6
rsync -avz --progress --partial file user@host:/pathrsync -avz --delete src/ user@host:/path/rsync -avz --exclude=".git" --exclude="node_modules" src/ dest/rsync -avz --backup --backup-dir=backup src/ dest/rsync -avz --max-size=100m src/ dest/rsync -avz --bwlimit=1000 src/ dest/Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
scp -r | Copies a directory tree but does not preserve permission bits or mtime | scp -r dir user@host:/path |
scp -P | Set the SSH port; capital -P, unlike lowercase -p in ssh | scp -P 2222 file user@host:/path |
rsync -a | Archive mode: preserves permissions, owner, timestamps and symlinks | rsync -a dir user@host:/path |
--delete | Remove files on the destination that no longer exist on the source | rsync -av --delete src/ user@host:/path/ |
--partial | Keep partially transferred files so the next run resumes instead of restarting | rsync -avz --progress --partial file user@host:/path |
Common pitfalls
SymptomA large rsync dies at 60% and the next run starts from 0% again.
Cause--partial is missing, so rsync deletes its temp file and cannot resume.
FixAlways use rsync -avz --progress --partial and re-run the same command after a drop.
SymptomAfter rsync -r dir user@host:/path the remote ends up with /path/dir/dir/...
CauseWithout a trailing slash rsync copies the directory itself as one item into the destination.
FixWrite dir/ to sync the *contents* of dir into /path - the only correct form for mirrors.
SymptomAfter rsync -av --delete, files that had been placed on the remote by hand vanished.
Cause--delete removes any destination entry missing from the source, regardless of who created it.
FixPreview with rsync -av --delete --dry-run src/ user@host:/path/, then add --backup --backup-dir=backup.
SymptomA directory uploaded with scp -r shows different permissions and timestamps on the remote.
Causescp -r copies recursively but does not transfer permission bits or mtime.
FixUse rsync -a instead - one command preserves permissions, owner and timestamps.
Symptomrsync fails with Permission denied (publickey) or connects to the wrong port.
Causersync has no -p port flag; the port and key must be handed to ssh via -e.
FixUse rsync -e "ssh -p 2222" or rsync -e "ssh -i key" src/ user@host:/path/.
Troubleshooting
1Preview a mirror sync before letting --delete run
rsync -av --delete --dry-run src/ user@host:/path/Every line marked deleting is what would be removed; drop --dry-run once it looks right.
2Resume a large file after an interruption
rsync -avz --progress --partial file user@host:/path--partial keeps the finished portion, so re-running the identical command continues.
3Non-default SSH port or a specific key
rsync -e "ssh -p 2222" file user@host:/pathPort and key belong to ssh itself, so they must be passed through -e.
4Sync source only, without dependencies and VCS metadata
rsync -avz --exclude=".git" --exclude="node_modules" src/ dest/Repeat --exclude as needed - it is the cheapest way to keep sync volume down.
Command Examples
Dry-run first, then sync for real
rsync -av --dry-run --exclude=".git" --exclude="node_modules" src/ user@host:/srv/app/
rsync -avz --exclude=".git" --exclude="node_modules" src/ user@host:/srv/app/rsync only ships what differs, so the first run is slow and every later run is nearly instant. The real habit is to always run --dry-run first: the deleting / creating lines in its output are exactly what will change or disappear on the remote host, so read them before dropping the flag.
Output
sending incremental file list file1 file2 sent 1,234 bytes received 56 bytes 2,580.00 bytes/sec total size is 12,345 speedup is 9.52
Mirror a directory and keep overwritten files
rsync -avz --delete --backup --backup-dir=backup src/ user@host:/srv/app/--delete makes the remote an exact mirror (anything extra on the far side is removed); --backup --backup-dir=backup moves the overwritten or deleted copy into a backup/ folder on the remote. Together they give you consistency without actual data loss.
Output
sent 512 bytes received 96 bytes 1,216.00 bytes/sec total size is 12,345 speedup is 20.30
Non-default SSH port plus an explicit key
rsync -avz -e "ssh -p 2222" src/ deploy@host:/srv/app/
scp -P 2222 -i ~/.ssh/id_ed25519 dist.zip deploy@host:/tmp/For rsync the port lives inside -e "ssh -p 2222" (an ssh option), while scp has its own capital -P 2222. Mixing them up is easy: -p in rsync is an rsync flag, and lowercase -p in scp means "preserve times" and quietly keeps the default port.
Resume a large transfer and cap bandwidth
rsync -avz --partial --progress --bwlimit=1000 bigfile.iso user@host:/data/--partial keeps the partial file after an interruption so the next run resumes instead of restarting from zero; --bwlimit=1000 caps throughput at 1000 KB/s so you do not saturate a shared office uplink. Neither changes what ends up on disk.
Verify a big upload with a checksum before deleting the source
rsync -avz --checksum dist.tar.gz user@host:/srv/releases/
md5sum dist.tar.gzA completed transfer is not proof of intact content. --checksum makes rsync compare by content instead of timestamps; then run md5sum locally and compare it with the remote copy. Only delete the source after they match — the only ordering you will not regret for a release tarball.
Common Pitfalls
- rsync --delete removes files on the target based on the source listing: one typo in the source path, a missing trailing slash, or an unmounted volume empties the remote directory (database files, user uploads and all) in a single run. Always inspect the deleting lines with --dry-run first.
- The trailing slash on an rsync source path is decisive:
src/syncs the contents of the directory, whilesrcsyncs the directory itself and you getdst/src/. That single character is the difference between a flat deploy and one extra nesting level. - scp -r preserves neither permission bits nor mtimes: after a recursive upload the remote mode comes from umask, so executable scripts can arrive non-executable, and every file looks "changed" to an incremental sync, forcing a full re-transfer. Use rsync -a when attributes matter.
- rsync has no built-in network resilience beyond -z and no retry logic: without --partial, an interrupted transfer restarts the whole large file. For public-network transfers set ssh ServerAliveInterval and pass options through -e "ssh -o ...".
- When --delete is used on the destination, --exclude patterns do not protect the excluded directory itself: excluded files are neither deleted nor updated there, which makes a sync look successful while the data is silently stale.
- Running rsync from Windows via Git Bash / MSYS (cwRsync, WSL paths) runs into path translation: arguments like
-P 2222can be rewritten as paths and the error points at a completely unrelated file. When a flag error looks nonsensical, echo the command and confirm the argument reached rsync unchanged.
Tips
- scp suits simple transfers; rsync suits directory sync and incremental transfer.
- rsync --delete removes extra target files; confirm the target has nothing important first.
- With rsync, src/ syncs the directory's contents, src syncs the directory itself.
FAQ
How does --update differ from --ignore-existing in rsync?
Both protect what is already on the receiving side, but they judge that very differently. --update compares timestamps: a destination file newer than its source is skipped, so only genuine source-side changes get through. That makes it the right choice for continuously syncing a working directory. --ignore-existing never looks at time at all — if the path exists on the destination it is left alone even when the source copy is newer. Use it when you are topping up a machine that already holds a lot of files and only want to add what is missing.
How do I keep the VCS metadata and dependency folders out of a code sync?
Add one --exclude per pattern: rsync -avz --exclude=".git" --exclude="node_modules" src/ dest/. The options can be chained as shown, and you can keep adding more. Patterns follow rsync wildcard matching, and matching a directory skips everything beneath it, so node_modules needs no rule of its own for its contents. Once the list grows past what the shell will take comfortably, move it into a file and use --exclude-from=exclude.txt — that also sidesteps quoting differences between shells.
How can I see how much was actually transferred versus skipped?
Pass --stats. The summary at the end reports the number of files transferred, literal bytes moved, total matched files and elapsed time, which tells you at a glance whether the run was a full resend or just a delta. If the numbers show the same files being flagged as different on every pass, the cheap size-or-mtime heuristic is not discriminating enough for that tree. Switch to --checksum to compare real content hashes at the cost of reading both copies, or take the middle road with --size-only, which ignores timestamps entirely and works well in directories where something else keeps rewriting mtimes.
Timestamps never line up and the same files get resent on every run. What fixes it?
Start by suspecting filesystem timestamp granularity. FAT-family filesystems record mtime in 2-second steps, while rsync compares with a finer window by default, so a file can look changed every single time. Widening the tolerance to one second resolves it: rsync -av --modify-window=1 src/ dest/. If the timestamps are actually accurate and the mismatch comes from somewhere else, leave the window alone and change what gets compared — use --checksum for real content hashes or --size-only for byte counts, which sidesteps timestamps entirely.
How should I layer include, exclude and --filter rules so they do not cancel each other?
Rules are matched top to bottom and the first hit wins, so anything after it is never consulted. The canonical "text files only" recipe is therefore an allow rule immediately followed by a catch-all deny: rsync -av --filter="+ *.txt" --filter="- *" src/ dest/. Writing --include="*.txt" on its own does not exclude anything — the rest of the tree still syncs, which is usually a surprise. Keep a reusable list in a file and point --exclude-from=exclude.txt at it; for a single pattern, --exclude="*.tmp" is the shortest form.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us