How to Use rsync Exclude Delete and Dry Run Flags Safely
Use rsync exclude, delete and dry run flags safely: patterns that match, preview with itemized output, delete only after a check, and protect the destination.

Syncing two directories is a one-way mirror, which means a mistake in the source path or an unexpected empty source turns into deleted data at the destination. Three options make rsync safe to run: dry run, careful patterns, and a limit on deletion. The examples use rsync 3.3; the basics, including the trailing slash, are in the rsync guide.
Always dry run first
rsync -an --delete --itemize-changes ~/site/ deploy@web1:/var/www/site/*deleting old-page.html
>f+++++++++ new-page.html
>f.st...... index.html
.d..t...... assets/
The first column describes the change. *deleting means a deletion, >f+++++++++ a new file, and >f.st...... a file whose size and time differ. Read the deletions especially. Then repeat the exact command without n. Keeping the flags identical, by storing the command in a script with a DRY=-n variable, avoids running something different from what was checked.
Exclude patterns that behave
rsync -a --exclude='/cache/' --exclude='*.log' --exclude='.git/' src/ dest/
rsync -a --exclude-from=exclude.txt src/ dest/Patterns match against the path relative to the transfer root. /cache/ with a leading slash matches only the top-level directory, while cache/ matches a directory of that name at any depth, which can hide a source folder you did need. A trailing slash limits the match to directories. Order matters, since the first matching rule wins, so put --include exceptions before the broader --exclude. Excluded files are not deleted from the destination by --delete unless you add --delete-excluded.
Put brakes on delete
rsync -a --delete-after --max-delete=50 src/ dest/--delete-after removes files only after the transfer succeeds, so an interrupted run does not leave a half-deleted tree. --max-delete=50 stops with an error if more than 50 deletions are planned, which catches the case of an unmounted source directory looking empty. A script should also verify that the source exists and is not empty before it syncs, for example with [[ -n $(ls -A "$src") ]]; see dry-run flags for destructive scripts.
Keep what gets replaced
rsync -a --backup --backup-dir="../backup-$(date +%F)" --delete src/ dest/Files that are overwritten or deleted are moved into the backup directory instead of vanishing. A relative --backup-dir is interpreted from the destination, so give a path outside it, or the backup is mirrored into itself on the next run. This gives a poor man's version history for little cost, though a snapshot tool is better for long-term retention, see restic and borg.
How rsync decides what to copy
By default rsync compares the size and modification time of each file on both sides and transfers files that differ. For those files it can use the delta algorithm: it splits the destination file into blocks, computes rolling and strong checksums, and sends only the blocks that changed. Over a slow link with large, slightly changed files, the saving is large. For a local copy, rsync turns the delta off and copies whole files, because reading both sides costs more than writing one. The flag -c forces a checksum comparison for every file, which is slow because it reads everything on both sides, and useful only when modification times cannot be trusted, for example after restoring from an archive that reset them.
Because the comparison uses time and size, a file that changed without changing either is missed. This is rare, but a restore from a backup with preserved timestamps can do it. When exactness matters, add a verification pass with rsync -rcn and look at the itemized list.
The trailing slash decides the layout
A source with a trailing slash means "the contents of this directory", and without one means "the directory itself". So rsync -a src/ dest/ puts the files of src directly into dest, while rsync -a src dest/ creates dest/src. Combined with --delete, the wrong choice is serious: if the intention was to mirror the contents and the slash is missing, the files go one level down and the real destination is cleaned of everything else. The dry run shows it at once, with a list of deletions that is far longer than expected. The trailing slash on the destination has no effect, but writing it is a reminder that it is a directory.
Protecting against an empty or missing source
The most damaging case is a source that is empty because a mount failed or a variable was unset, with a delete option pointing at a good destination. Guard in the script before the transfer: check that the source exists, is a directory, and contains at least the files you expect, for example with [[ -s $src/index.html ]]. Use ${src:?} so an empty variable stops the script. For mounts, test that the path is a mount point with mountpoint -q. rsync itself refuses to delete when it hits a read error on the source, unless told with --ignore-errors, which is why that option is dangerous in a mirror job.
Filters, ordering and the files you protect
Exclude and include rules are checked in order, and the first match wins. A rule list such as include one subdirectory and exclude everything else must put the include first, with the parent directories included too, or the subdirectory is never reached. With --delete, files matched by an exclude rule are also protected from deletion on the destination, which is usually what you want for a cache or logs directory there. The option --delete-excluded reverses that, and removes excluded files from the destination, so use it only on purpose. Test filter rules with the dry run and --itemize-changes, which makes the effect of each rule visible without any change to the data.
Running it unattended
For a scheduled job, set a lock so two runs do not overlap, use --log-file to record what happened, and check the exit status: 0 for success, 23 for a partial transfer due to errors, 24 for files that vanished during the run, which is normal on a busy source. Treat 24 as a warning and any other non-zero value as a failure. Keep a rotated history of destinations with --link-dest pointing at the previous run, which makes each snapshot look complete while unchanged files are hard links, and adds little space for each extra day.
Where this does not apply
Dry runs predict the plan, not the state when it runs; files can change between the two commands. --delete against a destination that other processes write to will remove their files, so keep mirrors dedicated. Some options behave differently with --files-from and with the daemon protocol. The full rules for filters are in the rsync filter rules section, and the listing format in the itemize-changes documentation.


