Everything for rsync in one place — pick a section below. 12 reviewed items across 4 content types.
Quick Answer
rsync's delta algorithm splits the destination file into fixed-size blocks, computes a fast rolling checksum plus a stronger checksum for each block, and sends only those checksums, not the file, to the sender. The sender then slides a window byte-by-byte over its version of the file looking for checksum matches, transmitting only the literal bytes that don't match any known block, so bandwidth is proportional to the size of the change, not the size of the file.
Detailed Answer
Imagine you and a friend each have a nearly identical thousand-page manuscript, and you want your friend's copy updated to match yours, but you're only allowed to mail a postcard's worth of text at a time. Instead of mailing the whole manuscript, your friend chops their copy into paragraphs, writes a short unique fingerprint for each paragraph, and mails you just the fingerprints. You then scan your own manuscript looking for paragraphs whose fingerprint matches one your friend already has — for those, you just say "keep paragraph 47 as-is" — and only mail the actual text for paragraphs that don't match anything on your friend's list.
This exists because naive file syncing tools re-transfer an entire file whenever any byte changes, which is wasteful for large files with small changes, think a 4GB virtual machine image where only a configuration file inside changed, or a log file that's mostly appended to. rsync's algorithm was designed specifically to make incremental sync bandwidth-efficient over slow or expensive links, and remains valuable today for large datasets over the internet, without requiring a full previous copy of the file to exist somewhere accessible to both machines simultaneously — only checksums need to cross the wire, not file content, for the parts that already match.
The receiving side splits its existing file into fixed-size blocks, commonly a few kilobytes tuned based on file size, and computes two checksums per block: a fast, weak rolling checksum, cheap to recompute as a sliding window moves forward one byte at a time, and a slower, strong checksum to avoid false-positive matches from the weak checksum alone. It sends this list of block checksums to the sender. The sender then scans its own version of the file with a sliding window, computing the same rolling checksum at every byte offset; when a rolling checksum matches one in the received list, it double-checks with the strong checksum before treating it as a real match, then instructs the receiver to copy its existing block instead of transmitting those bytes, and only sends the literal bytes for the byte ranges that never matched anything.
In production, rsync is commonly run over SSH for encrypted transport, with flags like --archive to preserve permissions, timestamps, and symlinks, --checksum to force full-file checksum comparison instead of relying on size and modification time, useful when clocks are unreliable, --bwlimit to cap bandwidth so a large backup sync doesn't saturate a shared link, and --exclude patterns to skip files that shouldn't be synced. What to monitor for large-scale sync jobs: transfer duration trending upward, a sign the delta algorithm is finding fewer matching blocks than expected, often because the destination file layout has drifted more than anticipated, and exit codes, since rsync returns distinct non-zero codes for partial transfer, connection failure, and vanished source files, and treating all non-zero exits identically hides very different failure classes.
The non-obvious gotcha experienced engineers hit is that the delta algorithm's efficiency depends entirely on byte-alignment surviving between versions — if a single byte is inserted or removed near the beginning of a large file rather than bytes just being overwritten in place, every block boundary downstream shifts by that amount, and the rolling checksum's whole purpose is to still find those shifted-but-unchanged blocks by sliding byte-by-byte rather than jumping in fixed increments. This works well for most real-world edits, but for files with structural reformatting, such as a JSON file re-serialized with different whitespace, the byte patterns can differ enough that rsync falls back to transferring nearly the entire file even though the logical content barely changed — a case where the algorithm's block-matching assumptions simply don't hold.
Code Example
# Sync a directory to a remote host over SSH, preserving permissions/times/symlinks (archive mode) rsync -avz --progress /var/lib/payments-api/data/ deploy@backup-host:/backups/payments-api/ # Perform a dry run first to see exactly what would transfer without changing anything rsync -avzn --delete /srv/checkout-worker/uploads/ deploy@backup-host:/backups/checkout-worker/ # Force full checksum comparison instead of relying on size+mtime (use when clocks may be unreliable) rsync -avz --checksum /var/log/user-auth-service/ deploy@backup-host:/logs/user-auth-service/ # Cap transfer bandwidth to 5MB/s so a large backup sync doesn't saturate a shared uplink rsync -avz --bwlimit=5000 /data/analytics-warehouse/ deploy@backup-host:/backups/analytics-warehouse/ # Exclude temp and cache directories from the sync to save bandwidth and avoid syncing junk rsync -avz --exclude='*.tmp' --exclude='cache/' /srv/checkout-worker/ deploy@backup-host:/backups/checkout-worker/
Interview Tip
A junior engineer typically answers that "rsync only copies the changed files," but for a senior or infrastructure-focused role, the interviewer is actually looking for you to explain the delta-transfer algorithm itself: rolling checksums enabling byte-level sliding comparison, why a weak-then-strong checksum pair is used instead of just one, and why this design avoids requiring both hosts to hold a full duplicate copy in memory. Bonus depth comes from explaining when the algorithm's efficiency breaks down, since byte-shifted or restructured files defeat block alignment, and from knowing operational details like distinguishing rsync's specific non-zero exit codes for partial transfer versus connection failure versus vanished files instead of treating any failure identically, which matters a lot when rsync is embedded inside an automated backup or deployment pipeline.
◈ Architecture Diagram
Receiver: splits file into blocks, sends checksums ┌────┐┌────┐┌────┐ checksums │ B1 ││ B2 ││ B3 │──────────────▶ Sender └────┘└────┘└────┘ Sender: slides window, matches blocks, sends only diffs ┌───────────────────────────┐ │ ...●●●●●●●●●●●●●●●●●●... │ ●=match ●=literal bytes sent └───────────────────────────┘
💬 Comments
Quick Answer
The most common causes are a broken or newly-added symlink loop, a source directory that grew unexpectedly with new large files or a runaway log, a change in file timestamps or permissions across the whole tree forcing rsync to re-transfer nearly everything instead of matching existing blocks, or a slow or degraded network path. Diagnose live by checking progress output for the current file and transfer rate, comparing source vs. destination directory sizes, and checking for recent bulk chmod/touch/chown operations on the source tree before assuming the transfer itself is broken.
Detailed Answer
This is like a mail-forwarding service that's supposed to only forward new letters, but suddenly every previous letter you ever received starts getting re-forwarded too — either someone re-addressed the entire filing cabinet by changing all the metadata, someone dumped a huge new stack of mail into the cabinet, which is real data growth, or the delivery truck itself is just moving slower than usual due to network degradation even though the amount of mail is normal.
rsync decides whether to skip a file entirely by default using a "quick check" of file size and modification time — if those match between source and destination, it assumes the file is identical and skips it without even reading the contents. This default exists because reading and checksumming every file's contents on every run would be extremely slow for routine incremental backups, so rsync optimizes for the common case where files that haven't been touched also haven't changed. But this same optimization is exactly why a bulk metadata change, such as an anti-virus scan that touches file access times, a permissions-fixing script, or a filesystem migration that resets modification times, can silently defeat the quick check and force rsync to re-transfer the entire tree even though the actual file content is unchanged.
To diagnose live without killing the job, first check what rsync is currently transferring using progress output or by inspecting the process's open file descriptors, and compare the current transfer rate against the historical baseline — a rate that's roughly normal but running for way longer points to volume growth, while a rate that's crawling points to network or disk I/O contention. Next, independently compare source and destination directory sizes to check whether the source genuinely grew, which rules in or out the new-or-large-files theory. Finally, spot-check a handful of files that should have been skipped, old, untouched files, to see if their modification time actually changed recently on the source side, which would confirm the quick check was defeated by a metadata-wide change rather than real data growth.
In production, mature backup pipelines log per-run summary stats, files transferred, bytes transferred, duration, so that three hours instead of ten minutes is immediately visible as an anomaly against history rather than something a human notices only when the disk fills up. Teams should also alert on destination disk usage independently from job duration, since a job that's still running normally but transferring far more data than usual can fill a disk well before the job itself would be flagged as abnormally slow. Using --stats on every run and shipping that output to a metrics system turns rsync into an observable pipeline component instead of a black box.
The non-obvious gotcha is that killing a long-running rsync job mid-transfer is often safe for the destination, since rsync uses temporary files and atomic renames by default, so a killed job leaves the destination in its last-known-good state rather than corrupted, but only if you haven't used --inplace, which writes directly to the destination file instead of a temp file for performance reasons — under --inplace, killing the job mid-write can leave a partially-overwritten, corrupted destination file, which is a dangerous trade-off many engineers make for speed without realizing the safety cost.
Code Example
# Watch live transfer progress and current file/rate without killing the running job watch -n 5 'ps aux | grep "[r]sync" ' # Inspect which file descriptor / file the running rsync process currently has open ls -la /proc/$(pgrep -f 'rsync.*payments-api')/fd 2>/dev/null | grep -v socket # Compare source vs destination sizes to check whether data genuinely grew du -sh /srv/payments-api/uploads/ && du -sh /backups/payments-api/uploads/ # Spot-check whether an "old" file's mtime actually changed recently (metadata-wide change indicator) find /srv/payments-api/uploads/ -name "*.log.2024*" -newer /tmp/marker-from-yesterday # Re-run with --stats to capture files/bytes transferred for baseline comparison going forward rsync -avz --stats /srv/payments-api/uploads/ deploy@backup-host:/backups/payments-api/uploads/
Interview Tip
A junior engineer typically says they'd just kill the job and re-run it, but for a senior or SRE role, the interviewer is actually looking for you to reason about rsync's quick-check optimization, size plus modification time, and how bulk metadata changes silently defeat it, forcing a full re-transfer that looks identical to "the job is broken" from the outside. Strong answers separate the three real causes, genuine data growth, a defeated quick-check from metadata changes, and network or disk degradation, using independent evidence such as source/destination size comparison, modification-time spot-checks, and transfer rate versus historical baseline, rather than guessing. Mentioning that --inplace trades transfer safety for speed, and that killing a job without --inplace is generally safe because of rsync's temp-file-plus-rename behavior, shows real operational judgment about when it's safe to intervene.
◈ Architecture Diagram
┌─────────┐ quick-check ┌─────────┐
│ size+mtm│────match───▶│ skip │
└─────────┘ └─────────┘
│ no match
↓
┌─────────┐ delta ┌─────────┐
│transfer │─────────▶│ temp + │
│ blocks │ │ rename │
└─────────┘ └─────────┘💬 Comments
Quick Answer
Alert on non-zero exit codes decoded by their specific meaning, such as code 23 for partial transfer due to file access errors, code 30 for timeout, and code 12 for protocol or connection error, rather than treating any non-zero exit the same, plus transfer duration and bytes-transferred trending far outside historical baseline, and, critically, periodic restore-verification tests, since a backup job reporting success only proves the transfer completed, not that the data is restorable.
Detailed Answer
This is like a fire extinguisher inspection program that only checks whether the extinguisher is physically present on the wall, never whether it still has pressure or whether anyone knows how to use it — the checklist gets marked "pass" every month right up until the day of an actual fire, when everyone discovers the inspection was checking the wrong thing entirely.
rsync's exit code is the first and cheapest signal, but it's frequently misused as a single boolean of success or failure when it actually carries specific meaning — the rsync manual defines distinct codes for partial transfer due to error, partial transfer due to vanished source files, a timeout waiting for data, and a protocol-level connection failure, among others. Treating a partial-failure code identically to a full connection-failure code means an alerting system either over-pages on minor partial failures or, worse, under-pages by suppressing all non-zero exits as "probably just a few flaky files," missing a real connectivity problem.
A properly instrumented rsync pipeline captures the exit code, parses --stats output, files transferred, total bytes, transfer rate, speedup ratio compared to a full copy, into structured metrics, and records duration per run so anomalies are visible as trend deviations, not just binary success or failure. Beyond the transfer job itself, the only way to actually know a backup is useful is to periodically restore a sample of files, or the whole dataset, into a scratch location and verify checksums against the known-good source — this is fundamentally different information from "the rsync command exited zero," because rsync exiting cleanly only proves the bytes it attempted to send were sent, not that the destination filesystem, storage backend, or retention policy hasn't silently corrupted or deleted them since.
In mature backup operations, teams run three tiers of checks: transfer-level, exit code classification and duration/byte trend alerting on every run, integrity-level, periodic full-content comparison runs since day-to-day syncs typically rely on the faster but less rigorous quick check, and restore-level, scheduled disaster-recovery drills that actually restore data to a scratch environment and validate it's usable, run monthly or quarterly depending on how critical the dataset is. Skipping the restore-level check is the single most common gap, because it's the only one of the three that would have caught a case where backups "succeeded" against a destination that was quietly misconfigured or running out of retained snapshot space.
The non-obvious gotcha is that rsync succeeding with a clean exit code says nothing about whether the destination itself is healthy — a destination filesystem silently running low on inodes, a cloud storage bucket lifecycle policy that's been deleting "old" backup files sooner than intended, or a destination that's actually a stale network mount serving cached directory listings can all make rsync report a clean, fast, successful transfer while writing into a location that isn't actually persisting the data correctly. Alerting purely on the rsync process's own exit code, without independently verifying the destination's actual state, misses exactly this class of failure.
Code Example
# Run rsync capturing both exit code and structured stats output for metrics ingestion rsync -avz --stats /srv/user-auth-service/data/ deploy@backup-host:/backups/user-auth-service/ \ > /var/log/rsync/user-auth-service-$(date +%F).log 2>&1 echo "EXIT_CODE=$?" >> /var/log/rsync/user-auth-service-$(date +%F).log # Decode common rsync exit codes explicitly instead of treating all non-zero the same # 0=success 12=protocol/connection error 23=partial transfer (errors) 24=vanished source files 30=timeout # Extract bytes transferred and speedup ratio from stats output for trend-based alerting grep -E "Total transferred file size|speedup is" /var/log/rsync/user-auth-service-$(date +%F).log # Periodic integrity check: force full checksum comparison instead of the faster quick-check rsync -avzn --checksum /srv/user-auth-service/data/ deploy@backup-host:/backups/user-auth-service/ | grep -c '^>f' # Restore drill: pull a sample back down and diff against source to prove restorability rsync -avz deploy@backup-host:/backups/user-auth-service/data/critical-file.db /tmp/restore-test/ \ && diff /srv/user-auth-service/data/critical-file.db /tmp/restore-test/critical-file.db
Interview Tip
A junior engineer typically says "alert if rsync's exit code isn't zero," but for a senior or SRE role, the interviewer is actually looking for you to decode specific exit codes into distinct incident types, partial file errors versus timeouts versus connection failures, rather than a single pass/fail signal, and, more importantly, to point out that a clean exit code only proves the transfer attempt completed, not that the backup is restorable. The strongest answers describe a three-tier verification strategy: transfer-level exit code and duration/byte-trend monitoring on every run, periodic full-checksum integrity runs since routine syncs rely on a faster and less rigorous quick check, and scheduled restore drills that actually pull data back and validate it. Mentioning that backup "success" and backup "restorability" are two different claims that require two different tests is the key insight interviewers are probing for.
◈ Architecture Diagram
┌──────────┐ ┌──────────┐ ┌──────────┐ │ Transfer │ │ Integrity│ │ Restore │ │exit code │ │ checksum │ │ drill │ │ + stats │ │ compare │ │ (verify) │ └──────────┘ └──────────┘ └──────────┘ daily weekly quarterly
💬 Comments
Quick Answer
Use rsync --delete carefully or avoid it entirely in favor of versioned/incremental destinations via --link-dest hardlink snapshots or a destination that supports point-in-time snapshots, always run a --dry-run diff before any destructive sync in automation, and keep multiple generations of backups rather than one continuously-mirrored destination, because a mirrored destination with --delete enabled will faithfully replicate a source-side deletion or corruption straight into your only backup.
Detailed Answer
A naive backup setup that just mirrors production into one destination folder is like having a single photocopier that instantly shreds the old copy the moment you photocopy a new page — if someone tears a page out of the original book by accident, your "backup" copier dutifully shreds that page from its copy too, because it was only ever designed to make the copy match the original exactly, not to remember history.
rsync --delete exists to keep a destination as an exact mirror of the source, which is genuinely useful for things like mirroring a website or a build artifact directory where you want the destination to reflect exactly what's on the source, nothing more and nothing less. But when the same flag is used for what's supposed to be a backup, a copy meant to survive mistakes made on the source, it defeats the entire purpose — a backup that mirrors the source in real time, including deletions, is not a backup against accidental deletion at all, only a backup against total destination loss.
A safer automation design uses rsync --link-dest pointed at the previous backup directory to create space-efficient historical snapshots: each run creates a new destination directory, and for any file that hasn't changed since the last snapshot, rsync creates a hardlink to the existing file in the previous snapshot directory instead of copying it again, so unchanged files cost zero additional disk space while still being independently protected per snapshot generation. Before any run that would use --delete, for cases where mirroring truly is intended, like a CDN origin sync, the automation should first run the exact same command with --dry-run and diff the planned deletions against an expected threshold — if a routine sync is suddenly about to delete 40 percent of files instead of the usual near-zero, that's a signal something upstream is wrong and the run should abort rather than proceed.
In production, the automation itself should be idempotent, safe to re-run after a partial failure without duplicating work or corrupting state, which for rsync is largely free since re-running a sync against a partially-completed destination just resumes where it left off. What needs explicit engineering is bounding blast radius: cap the maximum percentage of files a single run is allowed to delete or overwrite before requiring manual confirmation, retain a fixed number of historical snapshot generations with automatic pruning of the oldest rather than unlimited growth, and emit a structured audit record per run, source path, destination path, files added/changed/deleted, byte counts, exit code, so a security or platform team can reconstruct exactly what happened to any given file across time.
The non-obvious gotcha is that --link-dest snapshots only protect against file content being lost — if an attacker or a bad script targets the backup destination itself rather than the source, all those hardlinked snapshot directories share the same underlying inode data for unchanged files, so a deletion or ransomware-style encryption pass that touches the shared file across snapshots can silently corrupt what looks like multiple independent historical backups simultaneously. True protection against destination-side compromise requires either read-only or immutable storage for older snapshots, filesystem-level immutability flags or object storage with versioning and write-once-read-many policies, or replicating snapshots to a genuinely separate, less-privileged destination, not just relying on directory-level snapshot naming for safety.
Code Example
#!/usr/bin/env bash
# Idempotent, snapshot-based rsync backup for payments-api with safety guardrails
set -euo pipefail
SOURCE="/srv/payments-api/data/"
DEST_ROOT="/backups/payments-api"
TODAY=$(date +%F)
LATEST_LINK="${DEST_ROOT}/latest"
NEW_SNAPSHOT="${DEST_ROOT}/${TODAY}"
# Dry-run first and abort if the planned change is anomalously large (safety guardrail)
CHANGED=$(rsync -avn --delete "$SOURCE" "${LATEST_LINK}/" | grep -c '^deleting\|^>f' || true)
THRESHOLD=500
if [ "$CHANGED" -gt "$THRESHOLD" ]; then
echo "ABORT: $CHANGED changes exceeds safety threshold of $THRESHOLD" >&2
exit 1
fi
# Real sync: hardlink unchanged files from the last snapshot to save space, no source deletions applied here
rsync -avz --link-dest="${LATEST_LINK}" "$SOURCE" "$NEW_SNAPSHOT/"
# Update the "latest" pointer to the new snapshot for the next run's --link-dest base
rm -f "$LATEST_LINK"
ln -s "$NEW_SNAPSHOT" "$LATEST_LINK"
# Prune snapshots older than 30 days, keeping a bounded number of generations
find "$DEST_ROOT" -maxdepth 1 -type d -mtime +30 -name "20*" -exec rm -rf {} \;Interview Tip
A junior engineer typically reaches straight for rsync -a --delete because it "keeps the backup in sync," but for a senior or architect role, the interviewer is actually looking for you to recognize that a real-time mirror is not a backup against accidental deletion or corruption, it's the opposite, since it faithfully replicates the mistake. Strong answers describe --link-dest snapshot chains for space-efficient generational backups, a dry-run-and-threshold-check guardrail before any destructive sync runs unattended, bounded retention with automatic pruning, and structured audit logging per run. The most senior insight is recognizing that hardlink-based snapshots still share underlying data for unchanged files, so protecting against destination-side compromise requires immutability or a genuinely separate, less-privileged replication target, not just more snapshot directories.
◈ Architecture Diagram
day1: ┌─────────┐
│ Snapshot│
└────┌────┘
│ --link-dest (hardlink unchanged)
day2: ┌────↓────┐
│ Snapshot│──✓ new copy only for changed files
└────┌────┘
day3: ┌────↓────┐
│ Snapshot│
└─────────┘💬 Comments
Context
A media company moving 1.2PB of asset storage from an aging NAS to a new platform, where the naive plan — freeze writes, copy everything, cut over — implied a multi-day production freeze the business rejected outright.
Problem
The dataset was too large for any single-pass copy window; assets changed continuously (edit suites writing all day); and a partial/inconsistent cutover would corrupt project references spread across millions of files.
Solution
Iterative convergence: a first full rsync ran for nine days against live data (no freeze — accepting it captures a moving target imperfectly), then repeated delta passes each transferring only changes since the last (pass 2: 14 hours; pass 3: 3 hours; pass 4: 40 minutes) until the delta window converged to less than an hour of drift. The final pass ran inside a short write-freeze: quiesce writers, one last rsync (12 minutes), flip mounts, unfreeze. Throughout: --archive --hard-links --acls --xattrs for fidelity, per-tree parallel rsyncs (16 streams split by top-level directory) to saturate the link, and paired verification passes (--dry-run --checksum on samples) before declaring trees converged.
Commands
pass N: rsync -aHAX --delete --info=stats2 /nas/tree{i}/ /new/tree{i}/ # 16 parallel treesconverge check: rsync -aHAXn --checksum /nas/tree3/ /new/tree3/ | head # empty = converged
cutover: freeze writers -> final pass (12m) -> remount -> unfreeze
Outcome
1.2PB migrated with a 25-minute total write freeze instead of days; per-tree parallelism kept the 40GbE link at ~85% utilization; checksum sampling caught one tree where xattrs weren't landing (destination mount option) before cutover rather than after. The playbook became the org's standard for large moves.
Lessons Learned
The convergence-pass pattern is the whole trick — each pass bounds the next freeze window, and you cut over only when the math says minutes. Parallelize by directory tree, not by throwing -z or single-stream tuning at it; rsync is single-threaded per invocation and the filesystem walk is the bottleneck at this scale.
💬 Comments
Context
A research group needing nightly backups of a 40TB project share with long retention (180 days) and researcher-friendly restores — 'give me my directory as it was last Tuesday' — on a budget that made 180 full copies absurd and tape restores organizationally unusable.
Problem
Full-copy rotation cost 180x the data size; classic incremental chains (full + diffs) made point-in-time restores a reconstruction project; and the existing tar-based system meant restores went through one admin and took days.
Solution
rsync --link-dest snapshots: each night syncs against the live share into a new dated directory, passing yesterday's snapshot as --link-dest — unchanged files become hard links (costing a directory entry, not storage), changed files store fully. Every snapshot is a complete, browsable filesystem tree; storage consumption is one full copy plus daily churn (~0.7%/day). Retention is rm -rf on expired snapshot dirs (hard-link counts handle the rest); restores are cp -a (or researcher self-service via a read-only export of the snapshot tree). Job wrapped with locking, heartbeat, and an end-of-run verification comparing file counts and a checksum sample against the source.
Commands
rsync -aHAX --delete --link-dest=/backup/2026-07-10 /share/ /backup/2026-07-11/
retention: find /backup -maxdepth 1 -mtime +180 -exec rm -rf {} +restore: cp -a /backup/2026-07-08/projects/alpha ~/restored/ # or self-service RO mount
Outcome
180-day retention fits in ~2.3x the live data size (vs 180x for fulls); restores went from admin-ticket-plus-days to researcher self-service in minutes; and every snapshot being a plain directory tree means verification, browsing, and partial restores need no special tooling. The nightly window runs 40 minutes for the typical churn.
Lessons Learned
Hard-link snapshots demand inode headroom — the filesystem was provisioned with inode count in mind after a near-miss. The --delete flag inside a --link-dest scheme is safe (it shapes only the new snapshot), but the team documented it prominently because rsync --delete fear is well-earned elsewhere.
💬 Comments
Symptom
After what was intended to be a routine deploy/sync (rsync -a --delete release/ user@host:current), the destination directory current/ ends up containing far fewer files than expected, or is nested one level deeper than intended, and some previously-present files are gone.
Error Message
none — rsync completes 'successfully' since the command was syntactically valid, just semantically wrong
Root Cause
In rsync, a trailing slash on the source path changes the semantics: release/ copies the contents of release into the destination, while release (no trailing slash) copies the release directory itself into the destination, nesting it one level deeper. Combined with --delete, a mismatched trailing slash means rsync compares the destination against the wrong expected file set and deletes everything in the destination that doesn't match — which, if the source path was wrong, can mean deleting the entire prior contents of current/.
Diagnosis Steps
Solution
Stop the sync immediately if still running. If a previous release directory is still on disk (common in symlink-swap deploy patterns) or in a separate release history, restore service by pointing back to that known-good release. If not, restore the destination from the most recent backup.
Commands
rsync -a --delete --dry-run release/ user@host:current
rsync -a --delete --max-delete=50 release/ user@host:current
rsync -a --log-file=/var/log/rsync-deploy.log release/ user@host:current
Prevention
Always rehearse destructive rsync commands with --dry-run first, especially any command combining --delete with a variable/scripted path. Prefer --delete-after over --delete-before so the destination isn't left partially empty mid-transfer, add --max-delete=<N> as a circuit breaker so a wrong-path mistake can only delete a bounded number of files, and sync into a fresh release directory (blue/green) rather than directly into the live current/ path.
💬 Comments
Symptom
A large rsync transfer is interrupted midway (network drop, killed process, timeout), and the destination directory — which is actively being served/read by another process — is left with a mix of fully-updated and stale/missing files, causing inconsistent behavior for anything reading from it during the transfer.
Error Message
rsync error: unexplained error (code 255) at io.c(...) [sender=3.2.7]
Root Cause
rsync applies changes incrementally as it transfers; if the destination is the live path something else is actively reading (rather than a staging directory swapped in atomically afterward), an interruption partway through leaves readers seeing a mix of old and new/missing files for as long as the destination stays in that state.
Diagnosis Steps
Solution
Re-run the rsync transfer to completion against the same destination to bring it back to a consistent state (rsync's incremental nature makes this generally safe to resume), and going forward avoid syncing directly into a live-served path.
Commands
rsync -a --delay-updates release/ user@host:current
rsync -a --checksum --dry-run release/ user@host:current
ln -sfn /releases/<new> /var/www/current
Prevention
Sync into a separate staging directory and atomically swap it into place (e.g. via a symlink rename) only once the transfer completes successfully, rather than rsyncing directly onto the path being actively served. For unavoidable direct-target syncs, use --delay-updates so rsync stages files and does the final move as a fast, low-window batch rather than applying changes incrementally throughout the transfer.
💬 Comments
Symptom
The DR site's database restore drill fails: the dump file restores with a truncation error. Investigation widens — spot checks show the DR copies of several nightly dumps across the past month are corrupt, though most are fine. The rsync job's logs show success (exit 0) every night.
Error Message
Restore side: 'pg_restore: error: unexpected end of file'. rsync logs: clean completion, correct byte counts — for the bytes that existed while it was reading. One night's log shows the tell nobody watched for: 'WARNING: source file vanished/changed during transfer' style notices (rsync exit 24 on some nights, treated as success by the wrapper's 'exit code < 25' check).
Root Cause
The rsync window overlapped the dump window: the job copied dump files while pg_dump was still writing them, capturing torn intermediate states. rsync faithfully transferred what it read — a consistent copy of an inconsistent source. The wrapper compounded it by treating exit 24 (vanished files) as benign, and no restore verification existed until the drill; 'backup succeeded' had meant 'rsync exited' for a year.
Diagnosis Steps
Solution
Sequenced the pipeline properly: the dump job writes to a temp name and atomically renames on completion (rsync's default skips files mid-rename churn less, but the real fix—), the sync job now triggers on the dump's completion marker rather than a wall-clock guess, and copies only files matching the completed-dump pattern. Added automated restore verification: every night's shipped dump is test-restored into a scratch instance at the DR site, with the result as the backup's real success signal. Exit-code handling rewritten: 24 is a warning worth logging loudly, and any vanished-file event inside the dump directory fails the run.
Commands
pipeline: pg_dump -f dump.tmp && mv dump.tmp dump-$(date -u +%F).pgdump && touch DONE-$(date -u +%F)
sync: wait-for DONE marker; rsync -a --include='*.pgdump' --exclude='*' src/ dst/
verify: pg_restore --list dst/dump-$DATE.pgdump && nightly scratch-restore job
Prevention
Never sync live-written data without a consistency mechanism: completion markers, atomic renames, filesystem/LVM snapshots, or application quiesce. 'Transfer succeeded' and 'copy is usable' are different claims — only a restore test makes the second one. Audit wrapper exit-code handling; rsync's partial-success codes (23, 24) hide real problems in 'mostly worked'.
💬 Comments