Ops Journal · Practical notes on running software in production

rsync for backups: the flags that matter and the ones that quietly change what is copied

Published 2026-10-07 · 8 min read

rsync is the right tool for most file-level backups and the wrong tool to run from memory. A few of its defaults are surprising, and one of them — the trailing slash on the source path — changes what gets copied in a way that is easy to miss until you are restoring.

The trailing slash rule

This is the single most important thing to get right:

rsync -a /data/ /backup/data/     # copies the CONTENTS of /data into /backup/data
rsync -a /data  /backup/data/     # copies /data ITSELF, as /backup/data/data

A trailing slash on the source means "the contents of this directory". No trailing slash means "this directory, as an entry". Both are useful and the difference is invisible in the command unless you are looking for it.

The habit that prevents the mistake is to always use a trailing slash on both sides and be explicit about what that means, or to test with --dry-run before the first real run.

Always dry-run first

--dry-run (or -n) prints what would be transferred and changes nothing. Combined with -v it is a complete rehearsal:

rsync -avn --delete /data/ /backup/data/

Read the output before removing the n. This is especially important with --delete, where a wrong source path means the backup target gets emptied.

The archive flag, and what it actually includes

-a is short for -rlptgoD, and each letter matters:

Flag Preserves
-r recurse into directories
-l symlinks as symlinks
-p permissions
-t modification times
-g group
-o owner
-D device files and special files

-t is the one that makes rsync efficient: because modification times are preserved, the next run can skip unchanged files by comparing size and mtime instead of reading content. Without it, every run re-copies everything, which is correct and useless.

-o and -g only work when running as root. As a normal user they are silently ignored, which is worth knowing because a backup that looks complete may have lost ownership information.

The flags that change behaviour in ways that bite

--delete removes files at the destination that no longer exist at the source. This is what makes a backup mirror the source rather than accumulate history. It is also the flag that turns a typo into data loss, and it does not prompt.

# safe form: delete only what is missing, but keep a safety net
rsync -av --delete --backup --backup-dir=/backup/trash/ /data/ /backup/data/

--backup-dir moves anything that would be deleted into a separate directory instead of destroying it. For a first-time setup this is worth the disk space.

--exclude is evaluated against the transfer, and the pattern rules are not shell globs. A common error is --exclude '*.log', which works, versus --exclude 'cache', which excludes any file or directory named cache at any depth — usually what you want, occasionally a surprise.

rsync -av --exclude='*.tmp' --exclude='.cache/' /data/ /backup/data/

The trailing slash on .cache/ restricts the match to directories, which is the documented way to avoid matching a file of the same name.

--exclude-from reads patterns from a file, which is the maintainable form once there are more than a handful:

rsync -av --exclude-from=/etc/backup-excludes.txt /data/ /backup/data/

--checksum makes rsync compare content hashes instead of size and mtime. It is much slower and it catches the case where a file changed without its modification time changing — rare, but real on files restored from an archive. Use it for verification runs, not for every backup.

--partial keeps a partially transferred file so the next run resumes instead of starting over. For large files over a slow link this is the difference between a backup that finishes and one that never does:

rsync -av --partial --progress /data/ /backup/data/

-z compresses during transfer. It helps over slow links and costs CPU. On a fast local network it slows things down, which is worth knowing before adding it by habit.

The whole command, with the pieces that matter

rsync -aHv --delete --partial --exclude-from=/etc/backup-excludes.txt \
  --numeric-ids /data/ /backup/data/

-H preserves hard links, which matters for anything with a mail spool or a deduplicating store — without it, hard-linked files become separate copies and the backup can be much larger than the source. --numeric-ids prevents user and group names being mapped through the local name service, which keeps ownership correct when the backup is restored on a machine with different users.

Verifying, because a completed rsync is not a verified backup

rsync exiting zero means it transferred what it decided to transfer. It does not mean the destination matches the source. A verification pass compares the two directories without transferring:

rsync -avn --checksum --delete /data/ /backup/data/ | tail -20

With -n and --checksum, anything listed is a difference. An empty list means the two trees match by content, which is the check worth running after a backup that matters.

Running it on a schedule

Under systemd, a oneshot service with a timer is cleaner than a cron entry because the output lands in the journal:

# /etc/systemd/system/backup-data.service
[Unit]
Description=Back up /data

[Service]
Type=oneshot
ExecStart=/usr/bin/rsync -aHv --delete --partial \
  --exclude-from=/etc/backup-excludes.txt /data/ /backup/data/

Paired with a timer at a quiet hour, and with Persistent=true so a run missed while the machine was off happens at the next boot.

The two rules worth keeping

Dry-run with --delete until the output matches what you expect, and check the trailing slash on the source every single time. Those two habits prevent almost every rsync backup disaster, and both cost seconds.