Basic Copy
(archive) is the flag you almost always want: recursive, keeps permissions, times, owner, group, symlinks, and devices. The rsync command builder assembles one of these interactively.
| Command | What it does |
|---|---|
| Copy the contents of into , showing each file | |
| Copy the directory itself, so you end up with | |
| Copy one file | |
| With a per-file progress bar | |
| With one overall progress line for the whole transfer | |
| Dry run: show what would be copied, change nothing | |
| Print a summary of bytes sent and files transferred | |
| Skip files that are newer on the destination | |
| Only copy files that do not exist on the destination | |
| Only update files that already exist there | |
| Create missing destination directories (rsync 3.2.3 and newer) |
with no prints nothing on success, which is what you want in cron.
Trailing Slash
The one thing about rsync that catches everyone, and the reason behind most "it copied into the wrong folder" and " wiped my directory" reports. A trailing slash on the source path means "the contents of this directory". No slash means "this directory itself". The slash on the destination changes nothing at all.
| Command | Result |
|---|---|
| , | |
| , | |
| Same as the first row | |
| Same as , occasionally seen in scripts | |
| The contents of into | |
| Creates |
Read as including hidden files, and as "put a copy of this directory in there". Three consequences worth knowing before you run anything else on this page:
- With , prunes everything in , while only prunes and leaves the rest of alone. Getting that backwards is how people empty a directory.
- Tab completion appends the slash for you, so the command you typed and the command you tabbed are not the same command. Check the source path before pressing Enter, and dry run with .
- is not the same as . The glob is expanded by the shell, which skips dotfiles, so silently leaves out and .
Over SSH
Remote paths are . rsync must be installed at both ends. It runs over SSH by default, so keys, aliases, and agent forwarding all apply; see the SSH cheat sheet.
| Command | What it does |
|---|---|
| Local to remote (push) | |
| Remote to local (pull) | |
| Non-standard SSH port | |
| Specific key | |
| Port and key | |
| Use a Host alias from , which holds the port and key | |
| Through a jump host with | |
| Write as root on the remote side (needs passwordless sudo for rsync) | |
| Quote remote paths with spaces | |
| Same, without the shell interpreting the remote path () | |
| Several sources from the same host | |
| Give up if no data moves for 60 seconds |
Copy between two remote servers: rsync cannot take two remote paths in one command. Log into one and push to the other, or forward your agent so the first server can authenticate to the second.
ssh -A user@server1 'rsync -avz /var/www/ user@server2:/var/www/'A connection that fails with exit code 255 is an SSH problem, not an rsync one. See Connection refused and Permission denied (publickey).
Flags Explained
Paste any command into the rsync flag explainer to decode it.
| Flag | What it does |
|---|---|
| , | Same as : recursive, symlinks as symlinks, permissions, times, group, owner, devices |
| List each file as it is copied ( for more, for none) | |
| Compress in transit; helps over slow links, wastes CPU on a LAN or for already-compressed files | |
| Human-readable sizes in output | |
| : keep partial files for resuming, and show progress | |
| , | Show what would happen without doing it |
| , | Skip files that are newer on the destination |
| , | Compare by checksum instead of size and time (slow, but catches same-size edits) |
| Recursive; part of | |
| Copy symlinks as symlinks; part of | |
| Copy the file a symlink points to instead of the link | |
| , , , | Preserve permissions, times, owner, group; all part of |
| Preserve hard links (not in ) | |
| , | Preserve ACLs and extended attributes (not in ) |
| Stay on one filesystem; skip mounted volumes | |
| Itemize changes: a code per file showing what differed | |
| The remote shell command and its options | |
| Remove destination files that no longer exist in the source | |
| Skip matching files | |
| Cap bandwidth, in KiB/s by default ( for 5 MiB/s) | |
| One progress line for the whole run | |
| Transfer summary at the end | |
| Write what happened to a file |
is the standard remote copy, the standard local one, and when the link might drop.
Dry Run
Run every new rsync command with first, and always before .
| Command | What it does |
|---|---|
| List what would be transferred | |
| Also list what would be deleted, shown as | |
| Itemized dry run: one line per change with a code like | |
| Count and total size without copying |
In itemized output, is a file being sent, a directory being created, a deletion, and the dots after the type mark which attributes changed ( size, time, permissions, owner).
Exclude and Include
Patterns match relative to the top of the transfer. A trailing matches directories only, a leading anchors the pattern to the transfer root, and does not cross a ( does).
| Command | What it does |
|---|---|
| Skip every at any depth | |
| Skip files by extension | |
| Skip a directory (trailing slash means directories only) | |
| Skip only the top-level , not | |
| Several patterns; repeat the flag | |
| Same, using bash brace expansion (no spaces inside the braces) | |
| Patterns from a file, one per line | |
| Copy only files, keeping the directory tree | |
| Only files in the top level (no include, so no descent) | |
| As above, but prune empty directories | |
| Copy only the paths listed in a file, relative to | |
| Skip files over 100 MiB ( for the other end) | |
| Also delete files on the destination that an exclude now covers |
Include and exclude rules are checked in order and the first match wins, which is why comes before . An exclude that "does not work" is almost always anchored wrong: does not match when the transfer root is , but does.
Delete and Mirror
makes the destination match the source exactly, including removing files. Combined with a wrong path or a missing trailing slash it can empty a directory, so dry run first.
| Command | What it does |
|---|---|
| Mirror: copy changes and remove anything not in | |
| Preview the deletions first | |
| Refuse to delete more than 100 files (exit code 25 when it trips) | |
| Delete after the transfer, not before; safer for a live site | |
| Delete first, when the destination is short on space | |
| Move deleted and overwritten files into a dated directory instead of losing them | |
| Delete each source file after it is copied (a move); empty source directories remain | |
| Mirror but leave the destination's alone (excludes are protected from deletion by default) |
only deletes inside directories that are part of the transfer. (no trailing slash) syncs and never touches the rest of .
Progress and Resume
| Command | What it does |
|---|---|
| Keep partial files and show per-file progress | |
| One overall percentage, speed, and time remaining | |
| Keep partials in a hidden directory rather than in place | |
| Run again after an interruption; partial files continue from where they stopped | |
| Append to a partially transferred file and checksum the result; for single large files only | |
| Cap at about 5 MB/s so you do not saturate the link | |
| Cap at 50 MiB/s | |
| Abort if the transfer stalls for two minutes | |
| Run in the background and survive logout |
For a transfer that takes hours, start it inside or so a dropped SSH session does not kill it; see the tmux cheat sheet. If the SSH session itself keeps dropping, see Broken pipe.
Speed and Large Transfers
One rsync run is single-threaded and uses one SSH connection, so the way to go faster is usually to do less work or to run several transfers at once.
| Command | What it does |
|---|---|
| Copy whole files instead of computing deltas; faster on a LAN or between local disks | |
| Write changes into the existing file rather than a temp copy (VM images, tight disk space) | |
| Keep sparse files sparse instead of writing out the holes | |
| Do not spend CPU compressing files that are already compressed | |
| Cheap compression when the CPU, not the link, is the limit | |
| Use the fast xxHash checksum instead of MD5 (rsync 3.2 and newer) | |
| Build the whole file list up front so progress totals are real () | |
| Four transfers in parallel, one per top-level entry |
On trees with hundreds of thousands of small files, building and comparing the file list costs more than the transfer, and no flag fixes that. Split the tree and run several rsyncs, or the directory and copy one stream.
Compare and Verify
rsync decides what to send from size and modification time, not from file contents. These commands compare without copying, or force a content comparison.
| Command | What it does |
|---|---|
| List everything that differs, including files has and does not | |
| The same as an itemized list, one line per difference | |
| Compare by checksum, catching files with identical size and time but different contents | |
| Recopy anything whose contents differ, ignoring timestamps | |
| Treat same-size files as identical, for filesystems with unreliable times | |
| Copy only files that differ from a third reference directory | |
| Same, but take unchanged files from the reference locally instead of over the network | |
| Same, but hard link unchanged files from the reference | |
| Custom output: change code, name, and size per file |
rsync already checksums every file it transfers and retries any that fails, so a run that exits wrote correct data. is about deciding what to send, not about verifying what arrived.
Permissions and Ownership
| Command | What it does |
|---|---|
| Preserve permissions, times, group; owner too when running as root | |
| Set a fixed owner and group on everything copied (needs root at the destination) | |
| Set directories to 755 and files to 644 as they land | |
| Copy without permission or ownership bits (exFAT, SMB shares) | |
| Same for owner and group only, keeping permissions | |
| Recursive, links, and times, but no permissions or ownership at all | |
| Keep UID and GID numbers instead of mapping names (full system copies) | |
| Also copy ACLs and extended attributes | |
| Compare by size only, for filesystems that cannot store exact times | |
| Treat times within one second as equal (FAT stores 2-second stamps) |
Preserving owner means rsync on the receiving end runs as root, either because you are root or via . Otherwise files are owned by the connecting user and rsync prints nothing about it.
Backups and Snapshots
is what turns rsync into a backup tool: each run is a full dated tree, but files that did not change are hard links to the previous run and cost no extra disk. The rsync command builder has a link-dest field if you would rather assemble the command than type it.
| Command | What it does |
|---|---|
| A plain mirror backup, ACLs and extended attributes included | |
| Dated snapshot; unchanged files are hard links to the previous one | |
| Keep the previous version of anything overwritten or deleted | |
| Rename replaced files in place instead of moving them aside | |
| Offsite copy over SSH | |
| Keep the exclusion list in a file you can version | |
| Abort rather than mirror a source that has gone missing |
A hard-linked snapshot set shares inodes, so every snapshot of an unchanged file is the same blocks on the same disk. It protects against deleting a file, not against the drive dying. Keep one copy on another machine.
Cron and Logging
In cron there is no terminal, no ssh-agent, and a minimal , which is where nearly every "works by hand, fails in cron" report comes from.
| Command | What it does |
|---|---|
| Write a full transfer log | |
| Choose what each log line holds (time, operation, file, length) | |
| Fixed key, and fail instead of prompting for a password | |
| Skip this run if the last one is still going | |
| Run at the lowest disk and CPU priority | |
| Itemized change log without full verbosity |
# /etc/cron.d/backup
0 3 * * * root /usr/bin/flock -n /var/lock/backup.lock /usr/bin/rsync -a --delete \
-e "ssh -i /root/.ssh/backup_key -o BatchMode=yes" /srv/ backup@host:/srv/ \
>> /var/log/backup.log 2>&1Use absolute paths, a key with no passphrase (or a systemd timer with the agent available), and check rather than the output: prints nothing on success, so a silent cron job and a failed one look the same. turns a password prompt into an immediate exit 255, which is the behaviour you want when nobody is watching.
Daemon Mode
rsync can also run as its own service on TCP 873, with no SSH and no encryption. Remote paths then use a double colon or an URL, and the first component is a module name from , not a filesystem path.
| Command | What it does |
|---|---|
| List the modules a daemon offers | |
| The same, in the older syntax | |
| Pull from the module | |
| Push into it | |
| Non-standard daemon port | |
| Authenticate from a file (must be mode 600) | |
| The same without a prompt, from the environment |
# /etc/rsyncd.conf
[backup]
path = /srv/backup
read only = false
auth users = deploy
secrets file = /etc/rsyncd.secrets
hosts allow = 10.0.0.0/24One colon means SSH, two colons mean the daemon. Mixing them up gives or a hang on port 873. Daemon traffic is plaintext and the passwords are not SSH keys, so keep it on a private network and use the default SSH transport over the internet.
Common Recipes
Back up a home directory to an external drive:
rsync -avh --delete --exclude '.cache/' --exclude 'node_modules/' ~/ /Volumes/Backup/home/Mirror a website from a server, keeping a rotating set of dated snapshots that share unchanged files via hard links:
rsync -avz --delete --link-dest=/backup/www/latest user@host:/var/www/ /backup/www/$(date +%F)/
ln -sfn /backup/www/$(date +%F) /backup/www/latestDeploy a build, excluding what should not ship, then reload:
rsync -avz --delete --exclude '.git' --exclude '.env' --exclude 'node_modules' \
./dist/ deploy@host:/var/www/app/ && ssh deploy@host 'sudo systemctl reload nginx'Copy only files changed in the last day:
find src -type f -mtime -1 -printf '%P\n' | rsync -av --files-from=- src/ dest/Sync two directories both ways (run once in each direction; rsync is one-way, keeps the newer copy):
rsync -avu a/ b/ && rsync -avu b/ a/Move a whole server's data directory to a new host over a slow link, resumable:
rsync -avzP --bwlimit=20m -e "ssh -p 2222" /srv/data/ root@newhost:/srv/data/Clone a disk or root filesystem to another mounted volume, staying off , , and other mounts:
sudo rsync -aAXHvx --numeric-ids --exclude={'/dev/*','/proc/*','/sys/*','/tmp/*','/run/*','/mnt/*','/media/*','/lost+found'} / /mnt/newdisk/Windows and macOS
rsync is a Unix program. Windows has no native version, and the one Apple ships is not the rsync the manual describes.
| Situation | What to do |
|---|---|
| Windows 10 or 11 | , then ; your drives are under |
| Git Bash | No rsync included; MSYS2 () or Cygwin provides one |
| Windows to a Linux server | Run it from WSL: |
| Windows, local copy only | mirrors like , but cannot talk to an SSH server |
| macOS 14 and earlier | Ships rsync 2.6.9 from 2006; puts 3.x in |
| macOS 15 and newer | The bundled is openrsync, which rejects and other 3.x flags |
| Apple metadata | Apple's rsync takes for resource forks; the Homebrew build uses instead |
| Copying to exFAT, NTFS, or an SMB share | Add or everything recopies every run |
Check which one you have with . Under WSL, a copy from cannot preserve Unix permissions, so expect to warn and use there too.
rsync vs scp
| rsync | scp | |
|---|---|---|
| Second run of the same copy | Sends only changes | Sends everything again |
| Resume after a drop | Start over | |
| Delete extra files at the destination | No | |
| Exclude patterns | Yes | No |
| Installed by default | Usually, not on minimal images or stock Windows | Wherever SSH is |
| One file, once | (slightly quicker to type) |
OpenSSH's has used the SFTP protocol under the hood since 9.0, which fixed its security problems but did not add any of the above.
Two other comparisons come up often. is the tool when one side is cloud storage (S3, Google Drive, OneDrive, Backblaze), which rsync cannot speak at all. is the Windows built-in that mirrors local and SMB paths with , but has no SSH transport and no delta algorithm.
Exit Codes
rsync reports its result in the exit code ( right after it runs), which is what a cron job or a script should check.
| Code | Meaning |
|---|---|
| Success | |
| Syntax or usage error; a flag is wrong or unsupported by the remote rsync | |
| Protocol incompatibility; very old rsync on one end | |
| Errors selecting input files; usually a source path that does not exist | |
| Error starting the client-server protocol | |
| Socket I/O error; the connection dropped or the remote host closed it | |
| File I/O error; disk full or a read failure | |
| Error in the rsync protocol data stream; remote disk full, rsync missing on the remote, or a login script printing to stdout | |
| Errors with program diagnostics | |
| Killed by a signal (Ctrl+C) | |
| Partial transfer due to error; some files skipped, usually permissions | |
| Partial transfer due to vanished source files; benign for live directories | |
| Stopped by | |
| Timeout in data send or receive () | |
| Timeout waiting for the daemon connection | |
| SSH failed to connect; not an rsync error at all |
Gotchas
- with the wrong trailing slash or a mistyped destination removes real files. first, every time.
- rsync must exist on the remote host. from the far end and exit code 12 or 127 mean there, not here.
- A on the remote that prints something (a banner, a , an unguarded ) corrupts the protocol stream and gives or code 12. Guard it with at the top.
- macOS ships an old rsync 2.6.9, and macOS 15 replaced it with openrsync, which lacks flags such as . gives you 3.x; on Windows, rsync comes with WSL, Cygwin, or MSYS2, not with Git Bash by default.
- slows down a LAN copy and does nothing for files that are already compressed (images, video, archives). Use it over the internet, drop it locally.
- Copying to a drive formatted exFAT, FAT32, or NTFS from Linux recopies everything each run because the filesystem cannot store the same times and permissions. Add or .
- dies in zsh with when nothing matches, because zsh refuses an unmatched glob instead of passing it through the way bash does. Pick files with rather than a shell glob. On a remote source the glob is expanded by the remote shell, so it has to be quoted: .
- means the remote path is missing its colon ( instead of ) or you gave two remote paths in one command.
- Changing only a file's permissions or owner does not change its size or time, so a plain run does not resend it but does update the attributes. Content edits that keep the same size and time (rare, but and some build tools do it) need to be noticed.