If you move files around on Linux for a living, rsync is one of those tools worth learning properly rather than copying commands for. It’s fast, it’s everywhere, and once it clicks you stop reaching for cp and scp almost entirely.
This guide covers the flags you’ll actually use, the trailing slash rule that catches everyone at least once, how to read what rsync tells you it did, and how to build a backup you can restore from rather than one you hope works. If you’re setting up key-based access first, passwordless SSH, rsync and scp covers that side.
What rsync does that cp doesn’t
rsync copies files, but it’s cleverer about it than plain cp. It compares source and destination first, then sends only what actually differs. Copy a 40GB directory where one file changed and rsync moves that one file, not 40GB. That difference is the whole point of the tool, and it’s why it turns up in backup scripts everywhere.
By default it decides a file needs copying if the size or the modification time differs. That check is cheap and it’s right nearly all the time. When you need certainty instead of speed, -c makes it checksum every file, which is much slower and worth it maybe once a year to confirm a backup hasn’t quietly rotted.
For files that already exist at both ends, rsync goes further and sends only the changed blocks rather than the whole file. That’s the delta-transfer algorithm, and it’s why syncing a database dump that grew by a few megabytes doesn’t retransmit the whole thing.
The trailing slash, which everyone gets wrong once
This is the single most common rsync mistake, so it’s worth getting straight before anything else. A trailing slash on the source means “the contents of this directory”. No trailing slash means “this directory itself”.
rsync -av /home/asif/docs/ /backup/docs/ # docs contents land in /backup/docs/
rsync -av /home/asif/docs /backup/docs/ # creates /backup/docs/docs/
The second one isn’t an error. rsync did exactly what you asked. You just asked for something you didn’t want, and you usually find out a week later when the nested directory turns up in a restore.
The destination trailing slash makes no difference. Only the source matters. If you can’t remember which way round it goes, run it with -n first and read the paths in the output.
What archive mode actually turns on
-a is the flag you’ll use most, and it’s shorthand for a group of others:
| Flag | What it preserves |
|---|---|
-r |
Recurses into directories |
-l |
Copies symlinks as symlinks |
-p |
Permissions |
-t |
Modification times |
-g |
Group |
-o |
Owner, root only |
-D |
Device and special files, root only |
Preserving timestamps matters more than it sounds. Without -t every file looks new on the next run, so rsync recopies everything and you’ve thrown away the reason you used rsync in the first place.
Three things -a does not include, which surprises people restoring a backup and finding something missing:
-H preserve hard links
-A preserve ACLs
-X preserve extended attributes
If you’re backing up a system that uses ACLs or SELinux labels, you want -aAX at minimum. Plain -a will silently drop them. Worth knowing if you’ve set up ACLs with setfacl on the source.
Everyday examples
Local copy, keeping everything intact:
rsync -av /home/asif/documents/ /media/backup/documents/
Push to a remote server over SSH. rsync uses SSH by default now, so -e ssh is no longer needed unless you’re changing the SSH options:
rsync -avz /var/www/ asif@backup.example.com:/backups/www/
-z compresses in transit. Worth it over a slow link or for anything text-heavy. Less worth it for video, photos or already-compressed archives, where you’re burning CPU to compress something twice and it can end up slower.
Pull instead of push by swapping the arguments:
rsync -avz asif@backup.example.com:/backups/www/ /var/www/
If SSH is on a non-standard port, or you need a specific key:
rsync -av -e "ssh -p 2222 -i ~/.ssh/backup_key" /var/www/ asif@host:/backups/www/
One requirement people trip over: rsync has to be installed on both machines for remote transfers. If it isn’t, you get a confusing error about the remote command not being found, which looks like an SSH problem but isn’t. Details on getting SSH itself set up are in installing and configuring SSH.
Dry run first, every time
-n makes rsync do everything except actually write. Combine it with -v and you get the full list of what would happen.
$ rsync -avn --delete /home/asif/docs/ /backup/docs/
sending incremental file list
notes.txt
reports/q3.pdf
deleting old-draft.odt
sent 1,204 bytes received 78 bytes 2,564.00 bytes/sec
total size is 4,891,203 speedup is 3,815.29 (DRY RUN)
That (DRY RUN) at the end is your confirmation nothing was touched. If you don’t see it, the run was real.
Reading a dry run properly is the habit that separates people who trust their backups from people who find out at restore time. Especially watch the deleting lines.
Reading what rsync tells you with -i
Verbose mode gives you filenames. -i gives you filenames plus a code saying what changed about each one, which is far more useful when you’re checking whether a sync did what you expected.
$ rsync -avi /home/asif/docs/ /backup/docs/
sending incremental file list
>f+++++++++ newfile.txt
>f..t...... unchanged-content.pdf
>f.st...... report.odt
cd+++++++++ archive/
Those eleven characters look like line noise until you know the layout. Reading left to right:
| Position | Meaning |
|---|---|
| 1 | > received, < sent, c created locally, . no transfer, * message follows |
| 2 | f file, d directory, L symlink, D device, S special |
| 3 to 11 | What differed: checksum, size, time, permissions, owner, group, then ACL and xattr |
A row of plus signs means the file is brand new, so everything about it is a change. >f..t...... means only the timestamp moved, the content is identical. >f.st...... means size and time both changed, so the content genuinely differs.
That distinction is handy when a sync looks busier than it should. A screen full of ..t entries usually means something is rewriting timestamps without changing content, and you can go find out what.
Mirroring with –delete, carefully
--delete removes files at the destination that no longer exist at the source. It’s how you get a true mirror, and it’s the flag most likely to ruin your afternoon.
rsync -avn --delete /source/ /destination/ # look first
rsync -av --delete /source/ /destination/ # then commit
The failure mode worth understanding: if the source path is wrong and happens to be empty, rsync mirrors that emptiness. It deletes everything at the destination, correctly, because that’s what you asked for. A typo in a path or a mount that didn’t come back after a reboot is all it takes.
Two guards worth knowing:
--max-delete=50 # bail out if more than 50 files would go
--delete-after # transfer everything first, delete at the end
--max-delete is the cheap insurance. If a normal run deletes a handful of files and one day it wants to delete 4,000, something is wrong and you’d rather rsync stopped than finished.
Excluding what you don’t want
rsync -av --exclude "*.log" --exclude "node_modules/" /var/www/ /backup/www/
For anything more than a couple of patterns, put them in a file, one per line:
$ cat /etc/backup-excludes
*.log
*.tmp
node_modules/
.cache/
/var/www/uploads/thumbnails/
$ rsync -av --exclude-from=/etc/backup-excludes /var/www/ /backup/www/
A leading slash in a pattern anchors it to the transfer root rather than matching anywhere. So /var/www/uploads/thumbnails/ above only matches that one path, while .cache/ matches any directory of that name at any depth.
Big transfers over unreliable links
-P is --partial --progress together. Partial keeps whatever made it across when the connection drops, so the next run resumes instead of starting over.
rsync -avP /large/dataset/ asif@host:/backups/dataset/
For overall progress rather than per-file, --info=progress2 gives you one line for the whole transfer, which is much easier to read when there are thousands of files:
$ rsync -a --info=progress2 /large/dataset/ /backup/dataset/
12.43G 67% 48.21MB/s 0:02:14 (xfr#8432, to-chk=2104/14229)
If the transfer is saturating a link other people are using, cap it. The value is in KiB per second:
rsync -av --bwlimit=5000 /large/dataset/ asif@host:/backups/
For anything that runs long enough to outlive your SSH session, start it inside screen so a dropped connection doesn’t kill the transfer.
Snapshot backups with –link-dest
This is the rsync feature that earns its keep and rarely gets covered. --link-dest points at a previous backup. Any file that hasn’t changed since then gets hard-linked instead of copied.
The result is a directory that looks like a complete full backup, but only consumes disk for the files that actually changed.
#!/bin/bash
set -euo pipefail
SRC=/home/asif/
DEST=/backup/snapshots
TODAY=$(date +%F)
LATEST=$DEST/latest
rsync -aAX --delete \
--link-dest="$LATEST" \
"$SRC" "$DEST/$TODAY/"
ln -sfn "$DEST/$TODAY" "$LATEST"
Run that daily and you get one directory per day. Each one browsable and restorable on its own, with unchanged files shared between them.
$ du -sh /backup/snapshots/*
4.2G /backup/snapshots/2026-09-07
118M /backup/snapshots/2026-09-08
94M /backup/snapshots/2026-09-09
The first snapshot costs full size. After that you’re paying for the delta while still getting a full tree you can copy a single file out of. No restore chain, no incremental replay, no special tooling.
One caveat: hard links mean the copies share inodes on the same filesystem. It protects you from deleting or changing a file. It does not protect you from the disk dying. Snapshots are not offsite backups, and treating them as such is a mistake people make once.
Running it unattended
rsync behaves predictably and exits with a sane status code, which is what you want from something running at 3am. Check that code rather than assuming.
| Exit code | Meaning |
|---|---|
0 |
Everything worked |
23 |
Partial transfer, some files failed. Usually permissions. |
24 |
Source files vanished mid-run. Common and often harmless on live directories. |
30 |
Timeout waiting for data |
255 |
SSH failed. Not an rsync problem. |
A cron entry that logs and tells you when it fails:
0 3 * * * /usr/bin/rsync -aAX --delete /home/asif/ /backup/home/ \
>> /var/log/backup.log 2>&1 || echo "backup failed" | mail -s "rsync failed" asif@example.com
Exit code 24 is worth calling out because it turns up constantly when backing up anything live, like a mail spool or a busy web root. A file existed when rsync built its list and was gone by the time it got there. Nothing is broken. If you treat 24 as a failure your alerts will cry wolf and you’ll start ignoring them, which is worse than not having them.
One more for remote backups where you need root at the far end but don’t want to permit root SSH logins:
rsync -aAX --rsync-path="sudo rsync" /var/www/ asif@host:/backups/www/
rsync, cp or scp?
| Situation | Use | Why |
|---|---|---|
| One file, same machine | cp |
rsync adds nothing here |
| Repeated sync of the same tree | rsync |
Only moves what changed |
| One-off file to a server | scp |
Fewer keystrokes, though rsync works fine |
| Backups of any kind | rsync |
Resumable, verifiable, preserves attributes |
| Transfer that may get interrupted | rsync |
--partial means you resume rather than restart |
Worth knowing that scp is effectively deprecated. OpenSSH now routes it through the SFTP protocol by default, and upstream has been steering people away from it for years. If you’re picking one tool to get good at, rsync is the better investment.
Troubleshooting
| What you see | What it usually means |
|---|---|
| Files land in a nested duplicate directory | Missing trailing slash on the source |
| Everything recopies every run | No -t, so timestamps aren’t preserved |
command not found on a remote sync |
rsync isn’t installed on the remote host |
| Permissions or owners wrong after restore | Ran without root, or without -aAX |
| Exit code 23 | Some files unreadable. Check the log for which. |
| Exit code 24 | Source files changed mid-run. Usually fine. |
Transfer slower with -z |
Already-compressed data. Drop the flag. |
| Destination emptied unexpectedly | --delete with a wrong or unmounted source |
Where to go next
The natural next step is making these run without a password prompt, which is covered in passwordless SSH, rsync and scp. If you’re newer to the shell generally, Linux commands and directory structure covers the ground this assumes.
rsync has been around for decades and hasn’t needed replacing, which tells you something. Learn the handful of flags above and you’ve covered most of what you’ll ever need. The rest of the man page is there when you hit an edge case, and you will.
If you take one habit from this, make it the dry run. Every destructive rsync you’ll ever regret would have shown you exactly what it was about to do, if you’d asked it first.


