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.

SysOps·39 commands·Last updated 2026-07-21
scprsyncRemote Transferlinux

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:/path
Upload a file to remote
scp user@host:/path/file .
Download a file from remote
scp -r dir user@host:/path
Recursively upload a directory
scp -P 2222 file user@host:/path
Specify a port
scp -i ~/.ssh/id_rsa file user@host:/path
Specify a private key
scp -C file user@host:/path
Enable compression
scp -q file user@host:/path
Quiet mode
scp -v file user@host:/path
Verbose output

rsync Basics 8

rsync file user@host:/path
Sync a file to remote
rsync user@host:/path/file .
Sync a file from remote
rsync -r dir user@host:/path
Recursively sync a directory
rsync -a dir user@host:/path
Archive mode (preserves perms, timestamps...)
rsync -av dir user@host:/path
Archive mode with verbose output
rsync -az dir user@host:/path
Archive mode with compression
rsync -e "ssh -p 2222" file user@host:/path
Specify the SSH port
rsync -e "ssh -i key" file user@host:/path
Specify the SSH key

rsync Incremental 6

rsync -av --delete src/ user@host:/path/
Sync and delete extra files on the target
rsync -av --update src/ user@host:/path/
Sync only newer files
rsync -av --ignore-existing src/ user@host:/path/
Skip files already present
rsync -av --dry-run src/ user@host:/path/
Simulate (no actual transfer)
rsync -av --progress src/ user@host:/path/
Show transfer progress
rsync -av --partial src/ user@host:/path/
Keep partial files (resume)

rsync Filtering 5

rsync -av --include="*.txt" src/ dest/
Sync only a given type
rsync -av --exclude="*.tmp" src/ dest/
Exclude a given type
rsync -av --exclude=".git" --exclude="node_modules" src/ dest/
Exclude multiple directories
rsync -av --exclude-from=exclude.txt src/ dest/
Read exclude rules from a file
rsync -av --filter="+ *.txt" --filter="- *" src/ dest/
Use filter rules

rsync Advanced 6

rsync -av --compress src/ dest/
Compress during transfer
rsync -av --checksum src/ dest/
Compare files by checksum
rsync -av --size-only src/ dest/
Compare by size only
rsync -av --modify-window=1 src/ dest/
Modification-time window (FAT filesystems)
rsync -av --log-file=rsync.log src/ dest/
Write a log
rsync -av --stats src/ dest/
Show statistics

Common Patterns 6

rsync -avz --progress --partial file user@host:/path
Resume a large file
rsync -avz --delete src/ user@host:/path/
Mirror a directory
rsync -avz --exclude=".git" --exclude="node_modules" src/ dest/
Sync code (skip deps)
rsync -avz --backup --backup-dir=backup src/ dest/
Sync and back up changed files
rsync -avz --max-size=100m src/ dest/
Cap transferred file size
rsync -avz --bwlimit=1000 src/ dest/
Limit bandwidth (KB/s)

Parameter matrix

ParameterEffectExample
scp -rCopies a directory tree but does not preserve permission bits or mtimescp -r dir user@host:/path
scp -PSet the SSH port; capital -P, unlike lowercase -p in sshscp -P 2222 file user@host:/path
rsync -aArchive mode: preserves permissions, owner, timestamps and symlinksrsync -a dir user@host:/path
--deleteRemove files on the destination that no longer exist on the sourcersync -av --delete src/ user@host:/path/
--partialKeep partially transferred files so the next run resumes instead of restartingrsync -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

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

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

  3. 3Non-default SSH port or a specific key

    rsync -e "ssh -p 2222" file user@host:/path

    Port and key belong to ssh itself, so they must be passed through -e.

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

A 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, while src syncs the directory itself and you get dst/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 2222 can 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