rsync is the right tool any time you need to copy files efficiently — locally or over a network. Unlike cp or scp, it only transfers the parts of files that changed, compresses data in transit, and can resume interrupted transfers. Once you understand a handful of flags, it replaces a surprising number of ad-hoc backup scripts.

Basic Syntax

rsync [options] source destination

A simple local copy (mirrors cp -r):

$ rsync -a ~/projects/myapp/ /backup/myapp/

The trailing slash on the source is significant:

  • source/ — copy the contents of source into the destination
  • source (no slash) — copy the source directory itself into the destination
$ rsync -a ~/photos/ /backup/photos/
# Result: /backup/photos/img001.jpg, /backup/photos/img002.jpg ...

$ rsync -a ~/photos /backup/
# Result: /backup/photos/img001.jpg, /backup/photos/img002.jpg ...

Both produce the same final structure here, but the distinction matters when the destination already has other files in it.

Essential Flags

Flag Meaning
-a Archive mode: preserves permissions, timestamps, symlinks, owner, group. Equivalent to -rlptgoD.
-v Verbose: show which files are being transferred
-z Compress data during transfer (useful over slow networks)
-P Combines --progress and --partial — shows progress and resumes partial transfers
-n or --dry-run Simulate without making changes
--delete Delete files in the destination that no longer exist in the source
--exclude Skip files matching a pattern
--include Include files matching a pattern (overrides excludes)
-h Human-readable output sizes
--checksum Compare by checksum instead of timestamp+size (slower but more accurate)

The most common combination for a reliable sync:

$ rsync -avz --progress source/ destination/

Remote Transfers Over SSH

Add a remote host with the same colon syntax as scp:

Local → Remote:

$ rsync -avz -P ~/myapp/ user@server.example.com:/var/www/myapp/
sending incremental file list
index.html
assets/css/main.css

sent 45,231 bytes  received 86 bytes  18,126.80 bytes/sec
total size is 1,234,567  speedup is 27.27

Remote → Local:

$ rsync -avz user@server.example.com:/var/log/nginx/ ~/logs/nginx/

To use a non-standard SSH port or a specific key:

$ rsync -avz -e "ssh -p 2222 -i ~/.ssh/prod_key" ~/app/ user@server:/opt/app/

Always Dry-Run First

Before any destructive sync (especially with --delete), do a dry run:

$ rsync -avz --delete --dry-run ~/photos/ /backup/photos/
deleting 2023/old-vacation.jpg
./
2024/trip001.jpg

(dry run) sent 1,234 bytes  received 56 bytes

The output shows exactly what would happen — nothing is actually changed.

Excluding Files and Directories

$ rsync -avz --exclude='*.log' --exclude='.git/' ~/project/ /backup/project/

For a long exclude list, put patterns in a file:

$ cat .rsync-exclude
.git/
node_modules/
__pycache__/
*.pyc
.DS_Store
*.log
dist/

$ rsync -avz --exclude-from='.rsync-exclude' ~/project/ /backup/project/

Includes and excludes are evaluated in order — the first match wins. To sync only .py files from a directory:

$ rsync -avz --include='*.py' --exclude='*' src/ backup/

Mirroring with --delete

--delete makes the destination an exact mirror of the source by removing files that were deleted from the source:

$ rsync -avz --delete ~/docs/ /backup/docs/
deleting old-report.pdf
sending incremental file list
new-report.pdf

sent 234,567 bytes  received 1,234 bytes

Use this carefully — combined with --dry-run, it’s safe; without it, files vanish from the destination permanently.

--link-dest creates a new backup directory but hard-links files that haven’t changed from the previous backup. This gives you full snapshot history with minimal disk space:

$ DATE=$(date +%Y-%m-%d)
$ rsync -avz --link-dest=/backup/latest ~/data/ /backup/$DATE/
$ ln -sfn /backup/$DATE /backup/latest

Each dated directory looks like a full copy, but unchanged files are just hard links — no extra space consumed. This is the core of many professional backup strategies.

Bandwidth Limiting

On a shared connection, cap the transfer rate to avoid saturating the network:

$ rsync -avz --bwlimit=5000 ~/data/ user@server:/backup/

The limit is in KB/s — 5000 means 5 MB/s.

Common Real-World Patterns

Deploy a website to a server:

$ rsync -avz --delete \
  --exclude='.git/' \
  --exclude='node_modules/' \
  _site/ user@server:/var/www/mysite/

Backup home directory to an external drive:

$ rsync -avz --delete \
  --exclude-from="$HOME/.rsync-exclude" \
  $HOME/ /Volumes/Backup/home/

Mirror an S3 bucket locally (via AWS CLI, not rsync, but worth knowing the alternative exists):

$ aws s3 sync s3://my-bucket ./local-copy/

Transfer large files with resume support:

$ rsync -avz -P largefile.iso user@server:/downloads/

If the connection drops, run the same command again — rsync picks up where it left off.

rsync vs scp

  rsync scp
Incremental transfer Yes No
Resume on failure Yes (-P) No
Compression Yes (-z) Yes (-C)
Exclude patterns Yes No
Mirror with delete Yes No
Simpler syntax No Yes

Use scp when you need to copy a single file quickly. Use rsync for everything else.

Conclusion

The three flags to internalize are -avz (archive, verbose, compress), --delete (mirror the source), and --dry-run (preview before acting). Everything else — excludes, --link-dest, bandwidth limiting — is layered on top of those fundamentals. Once you’re comfortable with rsync, it becomes the default tool for any “move or sync files” task, whether it’s a one-off deployment or a nightly backup cron job.