HomeTopicsrsync & Delta Transfer › Dry Runs & Verification

Dry Runs and Verification: Making rsync Safe Before You Press Enter

rsync has no confirmation prompt. The moment you press Enter, it begins doing exactly what you asked — which, when the command includes --delete and the source path is wrong, means it begins efficiently destroying a destination at the speed of your disks. Every experienced administrator carries a story like this, their own or a colleague's, and the stories share a root cause: the command was run before it was read.

The antidote is not caution as a mood; it is caution as a procedure. rsync ships with everything the procedure needs — a rehearsal mode that costs nothing, an itemized report that says precisely what would happen and why, checksum verification for when metadata isn't proof enough, and logging fit for an audit trail. This article assembles those pieces into a discipline: preview, run, verify, log. By the end you will be able to read a dry run the way an auditor reads a ledger, and you will have a pre-flight checklist for the rsync commands that can hurt you. It is part of our rsync & Delta Transfer series.

--dry-run: The Rehearsal That Costs Nothing

Add -n (long form --dry-run) to any rsync command and the entire run is simulated: rsync builds the file list, applies your filters, performs the quick check on every file, plans deletions if --delete is present — and then changes nothing. No file is written, none removed, no attribute touched. Combined with -v, it prints what would have been transferred; the destination is exactly as it was.

Because a dry run performs the real analysis, it exposes the mistakes that matter before they cost anything: the trailing-slash slip that would nest dest/src/src/, the exclude pattern that silently matches nothing, the sync aimed at the wrong host, the --delete that plans to remove four thousand files because the source volume didn't mount. Each of these is a disaster in a real run and a curiosity in a dry run.

Honesty requires listing what a dry run does not promise:

  • It is a snapshot, not a contract. The preview describes the world at preview time. Files added, changed, or removed between your dry run and the real run will be handled by the real run without appearing in the preview. Keep the two close together in time.
  • It inherits your vantage point. Run the preview as the same user, on the same machine, with the same environment as the real job will use — a preview run as yourself does not validate what the root cron job will do with different permissions and a different SSH identity.
  • It sees what the quick check sees. By default "changed" means size or modification time differs, so a corrupted-but-same-size, same-mtime file looks clean to both the dry run and the real run. Content-level certainty needs --checksum, covered below.

Remember: a dry run is a rehearsal on the live stage, valid for as long as the stage doesn't change. Preview, read, and then run the real command promptly, from the same account, with the identical option string — the only difference should be the missing -n.

Decoding Itemized Output: -i Is the Whole Story

Plain -v tells you which files would transfer. The far better companion for previews is -i (--itemize-changes), which tells you which files and why — each line prefixed with a compact code string describing exactly what rsync intends. The codes look like line noise until someone explains the columns once; here is that explanation.

Each code is a fixed-width string, such as >f.st....... The first character says what kind of action this is, the second what kind of file it concerns, and the remaining columns each track one attribute:

Position Values you will see Meaning
1 — action > receive, < send, c create locally, h hard link, . no data transfer, * message Which way data moves — or, with ., that only attributes change; * introduces words like *deleting
2 — file type f file, d directory, L symlink, D device, S special What kind of object the line is about
3 onward — attributes c checksum/content, s size, t time, p permissions, o owner, g group, then niche columns (ACLs, extended attributes) A letter means that attribute differs and will be updated; . means it already matches; + across all columns means the item is brand new

With the columns in mind, the common specimens read fluently:

>f+++++++++  report.pdf      new file — receiver doesn't have it at all
>f.st......  data.csv        re-sent: size and mtime differ (normal edit)
>f..t......  notes.txt       re-sent: same size, mtime differs (touched?)
.f...p.....  script.sh       no data sent — only permissions corrected
.d..t......  archive/        directory mtime adjusted, contents untouched
cd+++++++++  logs/           new directory created
cL+++++++++  current -> v2/  new symlink created
*deleting    old/tmp.dat     --delete would remove this from the destination

Two of these deserve a second look. A tree full of .f...p..... or .d..t...... lines means no data is moving — rsync is only reconciling attributes, typical after a job ran without -p or -t (see flags that matter for why -t earns its keep). And an endless parade of >f..t...... on files nobody edits is the signature of a timestamp-precision mismatch — the cross-platform disease treated in rsync across platforms. The itemize string doesn't just preview; it diagnoses.

Reading a --delete Preview Like an Auditor

When the command includes --delete, the preview's *deleting lines are the ones that can never be un-run, so give them a dedicated pass. Capture the preview, then interrogate it:

# Capture the rehearsal
rsync -avin --delete /data/projects/ backup@host:/srv/mirror/projects/ | tee preview.txt

# How many deletions, and of what?
grep -c '^\*deleting' preview.txt
grep '^\*deleting' preview.txt | head -50

Then ask the auditor's three questions. Is the count plausible? You renamed one folder; the preview plans 12 deletions — fine. You changed nothing; it plans 4,812 — stop, something upstream is wrong. Are the deletions where you expect? Removals scattered thinly across the tree match normal churn; removals concentrated under one directory mean that directory vanished from the source — was that intentional, or is a mount missing? Do additions mirror deletions? A rename shows up as pairs (>f+++++++++ for the new path, *deleting for the old); mass deletions with no corresponding additions are the fingerprint of data actually disappearing. Five minutes with grep against a saved preview answers all three — and the saved file doubles as your record of what was approved.

The Pre-Flight Checklist

Here is the whole discipline condensed into a checklist. Keep it beside any rsync job that includes --delete; it reads as comments, so it can live at the top of the job script itself:

# ---- rsync --delete pre-flight checklist ----
# 1. READ the command aloud: source first, destination second.
#    Which machine loses files if I have them backwards?
# 2. CHECK the trailing slash: src/ = contents, src = the folder itself.
#    Does the destination layout expect which one?
# 3. PROVE the source exists and is populated (a mount that failed
#    to attach looks like an empty source — and plans mass deletion):
mountpoint -q /data                      || exit 1
test -e /data/projects/.sync-anchor      || exit 1
# 4. REHEARSE and record:
rsync -avin --delete /data/projects/ backup@host:/srv/mirror/projects/ | tee preview.txt
# 5. AUDIT the rehearsal: is the deletion count sane? clustered anywhere?
grep -c '^\*deleting' preview.txt
# 6. ARM the tripwire, then run for real — same command, -n removed:
rsync -avi --delete --max-delete=100 \
      /data/projects/ backup@host:/srv/mirror/projects/
# 7. VERIFY convergence: re-run the rehearsal; silence = success:
rsync -avin --delete /data/projects/ backup@host:/srv/mirror/projects/
# ---------------------------------------------

Step 3's second line is the sentinel file trick, cheap and nearly foolproof: place an empty marker file (here .sync-anchor) inside the real source directory, and make the script refuse to run unless the marker is visible. If the volume didn't mount, the marker is absent, the script exits, and nothing is deleted. Exclude the marker from the transfer if you don't want it mirrored. Step 6's --max-delete=100 is the tripwire for whatever the checklist missed: past one hundred deletions, rsync aborts rather than obeys.

Verification After the Run

The run finishing is not the same claim as the run succeeding. Three verification layers, cheapest first:

  • The convergence check (every run). Repeat the identical command with -n added — step 7 above. A correct mirror answers with silence; every printed line is an unfinished piece of business: a file that changed mid-transfer, an attribute the destination couldn't hold, a permission error. Empty output is a machine-checkable "done."
  • The stats sanity glance (every scheduled run). --stats appends transfer totals — files examined, files transferred, bytes moved. Log them and compare against the job's usual rhythm; a nightly job that normally moves 2 GB suddenly moving 200 GB, or zero, deserves a human before anyone trusts the mirror. The numbers themselves are explained in syncing over WAN links.
  • The checksum audit (periodically). Normal runs trust size and mtime, so at-rest corruption — a failing disk quietly rotting blocks under unchanged metadata — is invisible to them. rsync -avnc src/ dest/ replaces the quick check with a full content hash of every file on both sides and prints any file whose bytes differ. It reads everything on both machines, so it is slow and IO-hungry by design; schedule it monthly or quarterly, in a maintenance window, and treat any output as a disk-health investigation, not a sync problem. (In-flight integrity, by contrast, needs no extra step: every file rsync transfers is already verified against a whole-file checksum as part of the delta algorithm itself.)

Logging for Audit

An unattended job's log is its testimony. Two flags produce a useful one: --log-file=/var/log/rsync/mirror.log writes a timestamped record independent of wherever stdout goes, and --log-file-format='%i %n%L' makes each line carry the itemize code plus the file name — the same codes you can now read, preserved as evidence of exactly what changed, when. Rotate the logs like any other; months later, "when did that directory disappear from the mirror?" becomes a grep, not an archaeology project.

If your environment answers to auditors, aim the log at their questions before they ask: keep the saved preview.txt from any manually approved --delete next to the change ticket, retain job logs for the same period as other system logs, and let each run's final convergence check stand as the recorded proof that source and mirror agreed at completion. A sync regime that can produce those three artifacts on request passes most scrutiny without a single extra tool.

The other half of testimony is the exit code, which cron and schedulers act on. The ones worth knowing by heart: 0 — clean success. 23 — partial transfer: some files failed (permissions, vanished paths, full disk); the log has the specifics, and the mirror is incomplete until proven otherwise. 24 — source files vanished between listing and copying: routine on live trees, and most jobs rightly treat it as success with a footnote. 30 — timeout. Script accordingly: treat 0 and usually 24 as green, everything else as a page. Which brings us to the honest boundary of shell scripting: once a job needs retry policies, failure alerts a colleague can configure, and history a team can inspect, you are building a scheduler by hand. On Windows, that is the job Sysax FTP Automation exists for — scheduled transfers with built-in retry, error handling, and logging — the same preview-run-verify discipline, operated from configuration instead of cron.

Guard Rails for Scripts That Run Unattended

Everything so far assumed a human reading previews. Scheduled jobs have no human, so the script itself must hold the discipline. A skeleton that encodes the habits:

#!/bin/sh
# mirror.sh — one option string, guarded, logged
set -eu                                   # die on errors and unset variables

SRC="/data/projects/"
DEST="backup@host:/srv/mirror/projects/"
OPTS="-ai --delete --max-delete=100 --stats --log-file=/var/log/rsync/mirror.log"

test -e "${SRC}.sync-anchor" || exit 1    # sentinel: source really mounted?

rsync $OPTS "$SRC" "$DEST"                # the run
rc=$?
[ $rc -eq 24 ] && rc=0                    # vanished-file warnings are OK here

rsync -n $OPTS "$SRC" "$DEST" | grep . && exit 1   # convergence: any output = fail
exit $rc

The choices worth copying even if you rewrite the rest: set -eu turns an unset variable — the classic way "$SRC" becomes an empty string and the root of the filesystem becomes your source — into an immediate abort instead of a catastrophe. The option string is defined once, so the real run and the convergence check can never quietly drift apart (the failure mode of every script that pastes the command twice). Quoted variables survive paths with spaces. The sentinel test guards the mount; the exit-code handling separates benign 24 from genuine failure; and the final dry run converts "the mirror is probably fine" into a checked assertion. Wrap the whole thing in flock in the crontab line, as shown in mirroring patterns, and the job is as safe as shell gets.

The Version to Tell a Colleague

rsync will do exactly what you typed, immediately, so the professional habit is to make it tell you first. -n rehearses the entire run without changing anything; -i makes the rehearsal legible, one code string per file, first column the action, second the type, the rest the attributes that differ. Before any --delete: read the command aloud, check the slash, prove the source is mounted (sentinel file), rehearse, count the *deleting lines, arm --max-delete, run, then re-rehearse and expect silence. Add --stats to scheduled jobs and glance at the trend; run a -c checksum audit a few times a year; keep logs with itemize codes so the mirror can testify on its own behalf.

The rest of the series turns this discipline into working systems: mirroring and one-way sync patterns for the jobs themselves, and the flags that matter for the vocabulary underneath every command you'll preview.

Frequently Asked Questions

Does --dry-run really change nothing at all?
Correct — no files are written, deleted, or modified, and no attributes are touched. rsync performs the full analysis (file list, filters, quick check, deletion planning) and then stops short of acting. The only outputs are the report on your screen and whatever you tee into a file.
What does a code like >f.st...... actually mean?
Read it in columns: > means the receiver gets data, f means a regular file, and the letters s and t mean size and modification time differ — so this file will be re-sent because it changed in the ordinary way. Dots are attributes that already match; a code of all plus signs marks a brand-new item.
My job exits with code 24 — is that a failure?
Usually not. Code 24 means some source files vanished between rsync listing them and copying them, which is routine when syncing a tree that applications are actively writing. Most scripts whitelist 24 as success. Code 23, by contrast, means some files genuinely failed to transfer — read the log before trusting that mirror.
Should I use --checksum on every run to be safe?
No — -c reads and hashes every byte on both machines, which turns a thirty-second nightly job into hours of disk grinding for almost no added safety, since transferred files are already checksum-verified in flight. Use it as a periodic audit for silent at-rest corruption, monthly or quarterly, in a maintenance window.
What is the sentinel file trick for --delete jobs?
Put an empty marker file inside the real source directory and make the script exit unless the marker exists. If the source volume ever fails to mount, the marker is missing, the script refuses to run, and the empty mount point can't masquerade as "the source deleted everything." It is one line of shell that prevents the classic rsync disaster.
How can I prove a completed mirror is actually correct?
Re-run the exact command with -n added: a converged mirror prints nothing, and any output names the discrepancy. For deeper assurance, an occasional rsync -avnc pass compares full file contents on both sides. Silence from both checks is as close to proof as file transfer offers.

From the Sysax team: we build secure file transfer software for Windows — Sysax Multi Server, an FTP, FTPS, SFTP, and HTTPS server, and Sysax FTP Automation for scheduled, scripted transfers. Free trials are on the download page.