Home › Topics › Bulk Moves › Rsync Moves

Rsync for Bulk Moves and Seed-and-Delta Migrations

When the data being moved lives on Linux, on a NAS appliance, or on anything reachable over SSH, the migration engine is rsync. Like robocopy on Windows, it is free, already installed almost everywhere, and restartable by nature. And it has one habit that makes it the natural seed-and-delta tool. Run the same rsync command twice, and the second run transfers only what changed.

This article uses rsync strictly as a migration engine. It covers the big seed pass and the delta passes that shrink toward zero. It handles the --delete flag with the respect a deleting flag deserves. It explains what attribute fidelity you actually get and how to choose between a local move and a move over SSH. Our dedicated rsync series already covers rsync's everyday depths — the rolling-checksum algorithm, the full flag tour, mirroring patterns. We will link into it rather than repeat it.

This is the rsync half of our Bulk Internal Moves with Robocopy and Rsync series. If your move is Windows-to-Windows, the parallel article is robocopy for server migrations. The phase plan both engines serve is in the series opener.

Why Rsync Fits the Migration Job

Three properties do the work. First, idempotent reruns: rsync compares every source file against the destination — by size and modification time, cheaply — and touches only files that differ. An interrupted seed resumes by rerunning it; a delta pass is just the same command run again later. Second, reach: rsync moves data between local disks, to and from mounted shares, and across the network through SSH. So one tool covers array-to-array, server-to-server, and site-to-site legs. Third, fidelity switches: permissions, ownership, symlinks, hard links, ACLs, and extended attributes each have a flag. So you decide — explicitly — how much of the file's identity travels.

Rsync is famous for transferring only the changed parts of big files with its rolling-checksum delta algorithm. That matters less inside a building than people assume, as we will see when choosing local versus SSH transport. The algorithm itself is beautifully explained in how the rsync algorithm works.

Reach deserves one more sentence, because it decides tool choice in mixed shops. Rsync is native on Linux and macOS, and present or installable on practically every NAS appliance. It is available on Windows only through ports and compatibility layers — workable, but not the path of least resistance. The practical rule: data living on Unix-family systems and appliances moves with rsync. Windows-to-Windows data moves with robocopy. And a mixed migration simply uses each engine on its own turf, meeting at a common share or an SSH endpoint. The cross-platform wrinkles that appear at that meeting point are covered in rsync across platforms.

The Seed Pass

The seed is the everything-once pass, run while users keep working. The baseline command for a local move — say, from an old array to a new one mounted on the same host — looks like this:

rsync -a -x --info=progress2 /srv/data/ /mnt/newarray/data/

Three choices are packed in there. -a is archive mode, the fidelity baseline unpacked in the next section. -x stays on one filesystem — it refuses to descend into other mounts nested under the source. That keeps a stray mounted backup volume from being "migrated" along for the ride. --info=progress2 prints whole-job progress — total bytes, overall percentage, current rate — instead of a per-file ticker that is useless across three million files.

Mind the trailing slash, the one piece of rsync syntax that ruins migrations. /srv/data/ with the slash means "the contents of data." /srv/data without it means "the directory itself." That lands everything one level deeper at the destination as /mnt/newarray/data/data/. Every rsync pass in a migration should come from a reviewed script, precisely so this character is typed once, checked once, and never fat-fingered at midnight.

Excludes belong in the seed command from day one. Every share carries cargo that should not travel — cache directories, .tmp litter, trash folders, application scratch space. The --exclude patterns (--exclude='.cache/', --exclude='*.tmp', or a shared --exclude-from=migration-excludes.txt file) keep it home. The governing rule is the same as on the Windows side: every exclusion is written down in the migration plan. Verification must be able to tell "excluded on purpose" from "missing by accident."

Two practicalities matter. A seed that runs for days must survive your laptop disconnecting. So run it inside a terminal multiplexer or under nohup on the server itself. And expect imperfection — the source is live, files will vanish mid-pass and change mid-copy. The seed's job is bulk, not perfection; the delta passes and the freeze exist to make it perfect later.

What -a Preserves — and What It Misses

-a is shorthand for -rlptgoD: recurse into directories (r), copy symlinks as symlinks (l), preserve permissions (p), modification times (t), group (g), owner (o — effective only when running as root), and devices and special files (D). That is an excellent default, and for many migrations it is enough. But archive mode has three well-known blind spots, each with its own flag and its own honesty clause:

  • -H — hard links. Without it, two directory entries pointing at the same file body arrive as two independent full copies: sizes balloon and the link relationship is gone. With it, rsync must remember every linked inode it has seen, which costs memory and time on huge trees. Turn it on when the source uses hard links (build trees, snapshot-style backup folders); leave it off when it does not.
  • -A — ACLs. Copies POSIX access control lists — the fine-grained permission entries beyond owner/group/other. Both filesystems must support them, and they do not survive translation to foreign permission models. An ACL that exists on the source filesystem cannot be represented on a destination mounted over SMB, for instance. If your move crosses permission models, plan a permissions rebuild instead of trusting translation — permission models explained covers why the models do not map one-to-one.
  • -X — extended attributes. Copies xattrs: SELinux security contexts, user-defined metadata, platform-specific extras. Support varies by filesystem and platform, and some attributes are meaningless once they land somewhere else.

Hence the honest reading of the popular -aHAX incantation: it is the maximum-fidelity request, not a guarantee. Each extra letter only delivers where both ends can hold what it carries. Two companions matter on cross-system moves. Run as root (or with sudo) on the receiving side if ownership is to be preserved at all. And add --numeric-ids when user and group names do not exist identically on both machines. That keeps files' numeric owner IDs instead of remapping them by name. Then verify your choices on a small test tree with ls -l and getfacl before trusting them with four terabytes.

Remember: decide fidelity during inventory, not after cutover. "Do we need hard links, ACLs, xattrs?" is a five-minute question before the seed and a forensic nightmare after users are on the new server.

Delta Passes That Shrink Toward Zero

After the seed, rerun the same command nightly. Each pass walks the tree, skips everything whose size and modification time match, and transfers the rest. Because users only touch so much data per day, the passes converge fast. Here is the shape from a realistic move — a few terabytes in about three million files:

Pass When Files moved Data moved Duration
Seed First week, nights 2.9 million 3.8 terabytes ~60 hours total
Delta 1 Three nights later 41,000 63 gigabytes 3 h 10 m
Delta 2 Next night 12,400 21 gigabytes 1 h 05 m
Delta 3 Next night 9,800 14 gigabytes 54 m
Delta 4 Next night 9,100 13 gigabytes 51 m
Final, frozen Cutover night 3,100 4 gigabytes 22 m

Read the pattern, because it is the plan. The passes shrink rapidly, then plateau — deltas 3 and 4 are nearly identical. They have converged on one day of churn plus the fixed cost of scanning three million directory entries. That plateau number is the treasure: it predicts how long the final pass will take under the write freeze. That is exactly how long users must tolerate a read-only share on cutover night.

If your deltas are not shrinking, something specific is wrong. Timestamps may be drifting, so unchanged files look changed — a classic cross-platform trap covered in bulk move pitfalls. An application may be rewriting whole trees nightly, or your passes may be too far apart. Diagnose before cutover; a delta that will not converge means a freeze window you cannot predict.

A note on how "changed" is decided: the nightly comparison uses size and modification time, which is fast and right for delta detection. Rsync also offers -c (--checksum), which compares file contents instead. It reads every byte on both sides to do it, turning a forty-minute delta pass into a day-long crawl. Do not use it for routine deltas; save content comparison for the verification phase, where its cost buys proof rather than convenience.

--delete, With Respect

Delta passes as described so far only add and update. But users also delete and rename, and without help the destination accumulates ghosts. These are files removed from the source weeks ago, still present on the new server, waiting to confuse verification and users alike. The cure is --delete: remove from the destination whatever no longer exists at the source.

That is mirroring, and it deserves the same fear robocopy's /MIR does. Point the command at the wrong target — or get the trailing slash wrong so trees misalign — and --delete will faithfully erase whatever the mistake implies. The discipline that keeps it boring:

# 1. Dry run first: -n changes nothing, -i itemizes every action
rsync -aH -x -n -i --delete /srv/data/ /mnt/newarray/data/ | less

#    lines beginning "*deleting" are the files --delete would remove
#    review them: do they match what users actually deleted?

# 2. Only then, the real pass
rsync -aH -x --delete --info=progress2 /srv/data/ /mnt/newarray/data/

The dry run costs one tree scan and buys you a complete preview. -n (--dry-run) makes no changes. And -i (--itemize-changes) prints one coded line per action, with deletions unmistakably marked. Reading those codes fluently is a skill worth having before cutover night — rsync dry runs and verification teaches it. And rsync mirroring patterns covers the mirroring idiom in full. That includes safety variants like --backup that stash deletions instead of discarding them.

Remember: --delete never runs un-reviewed. First pass with -n -i, read the *deleting lines, then run it for real — every time, even at 2 a.m. Especially at 2 a.m.

Local Move or Over SSH?

Bulk moves come in two transports, and rsync behaves differently in each — knowing why saves you from tuning the wrong thing.

Local moves — array to array on one host, or into a share mounted from the new server — are the simple case. Here is the honesty most people miss: when both paths are local, rsync skips its famous delta algorithm entirely and copies changed files whole. The -W option, whole-file, is the default locally. That is correct behavior, not a limitation. Computing deltas requires reading the file on both sides, which on local disks costs more than just copying it. Locally, rsync is "a very careful cp with skip-unchanged superpowers," and that is exactly what a migration needs.

Moves over SSH — rsync -aH -x --info=progress2 /srv/data/ root@newserver:/srv/data/ — add encryption and, on repeat passes, the real delta algorithm: only changed portions of changed files cross the wire. Two honest tradeoffs. On a fast LAN, SSH's encryption can become the bottleneck. A single CPU core encrypting the stream may cap throughput below what the network could carry. It is usually still fast enough, but measure before promising dates. Over a WAN, the tradeoffs invert: the link is the bottleneck, and delta transfer and resumability earn their keep. The --partial option (better, --partial-dir=.rsync-partial) keeps interrupted large files so the next pass completes them instead of restarting. The --bwlimit option caps the rate during business hours, and compression helps only for compressible data. The WAN discipline has its own article, rsync over WAN. For trusted in-building links where even SSH is optional, rsync SSH vs daemon mode weighs the alternative.

Interruptions and Exit Codes

Seeds get interrupted — reboots, dropped sessions, full disks. The recovery is rsync's whole design: run the same command again, and completed files are skipped in the scan. What you must read afterward is the exit code, because a migration script has to distinguish three endings:

  • 0 — clean pass, nothing failed. What you demand from the final frozen pass.
  • 23 — partial transfer: some files could not be read or written (permission denied, I/O errors). Investigate; the per-file complaints are on stderr or in --log-file output.
  • 24 — source files vanished mid-pass. On a live source this is routine — users delete things while you copy — so treat it as a warning during seed and delta passes. On the final pass under a write freeze it is a red flag, because nothing should be changing at all.

A wrapper script therefore accepts 0 always, tolerates 24 before the freeze, and fails loudly on everything else. Add --log-file=/var/log/migration/pass_delta3.log to every pass so each run leaves evidence for the verification step to consume.

Putting It Together

The commands a real rsync migration settles on, assuming a move over SSH with hard links in play:

# Seed - run under nohup/tmux, repeat until it completes cleanly
rsync -aH -x --info=progress2 --partial-dir=.rsync-partial \
      --log-file=/var/log/migration/seed.log \
      /srv/data/ root@newserver:/srv/data/

# Delta (nightly) - preview deletions, then mirror
rsync -aH -x -n -i --delete /srv/data/ root@newserver:/srv/data/ \
      > /var/log/migration/delta_preview.txt
# review *deleting lines, then:
rsync -aH -x --delete --info=progress2 \
      --log-file=/var/log/migration/delta.log \
      /srv/data/ root@newserver:/srv/data/

Adjust fidelity flags (-A, -X, --numeric-ids) to what inventory said you need, and rehearse the pair on a test tree until the output holds no surprises.

Where Rsync Hands Off

Rsync carries the move: seed, shrinking deltas, and the final frozen pass of cutover night. The counts and difference reports of verification follow. Where it hands off is the day the migration ends and a permanent job begins. Suppose two sites must stay synchronized from now on — the branch office mirror, the nightly feed to the DR location. That is standing scheduled synchronization, not a bulk move. It needs a schedule, retries, history, and someone alerted when a night fails. That job class is what Sysax FTP Automation is built for. Its wizard generates mirror, backup, and two-way synchronization tasks over SFTP, FTPS, or FTP, scheduled, with email notification on failure. And when a recurring flow must cross an untrusted network to or from Windows systems, run it against a secure transfer server. An example is Sysax Multi Server (SFTP, FTPS, and HTTPS, with activity logging). Use that approach rather than exposing file shares to the world. The dividing line between one-time replication and standing transfer jobs is drawn carefully in replication vs transfer jobs.

Frequently Asked Questions

Does rsync's delta algorithm speed up local copies?
No — when source and destination are both local paths, rsync copies changed files whole by default. Computing deltas would mean reading the file on both sides, which costs more than copying it. The delta algorithm pays off over network links, especially slow ones, on repeat passes of large changed files.
What does the trailing slash on the rsync source mean?
With a trailing slash, /srv/data/ means "the contents of data." Without it, /srv/data means "the directory itself," which lands one level deeper as data/data at the destination. Script the command once, check the slash once, and reuse the script for every pass.
Is rsync -a enough to preserve everything?
-a preserves permissions, times, group, owner (as root), symlinks, and special files — but not hard links (-H), ACLs (-A), or extended attributes (-X). Add those flags if your data uses those features, and remember each one only works where both filesystems support what it carries.
When is it safe to add --delete?
Once the seed is complete and you are running delta passes, --delete keeps deletions and renames in sync. Run it only from a reviewed script, and precede every real pass with a dry run (-n -i). That lets you read the *deleting lines before anything is removed.
What do rsync exit codes 23 and 24 mean?
23 means a partial transfer — some files failed, usually permissions or I/O, so read the log. 24 means source files vanished during the run, which is normal on a live share but should never happen on the final pass under a write freeze. Scripts should treat 0 as success, 24 as a pre-freeze warning, and everything else as failure.

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