MIGRATION · April 24, 2026 · 13 min read
Cutting SharePoint migration time in half: what faster cutovers mean for your business
A migration window is not an abstract benchmark. It is the hours your team cannot edit documents, the evening your admins spend babysitting a progress bar, and the risk that a Sunday cutover bleeds into Monday morning with users waiting. When a SharePoint migration completes in about half the time of a single-threaded transfer, the benefit lands on the calendar first and in the engineering notes second.
This post walks through what a faster cutover actually changes for the business, then explains why SharePoint migrations on MigrationFox complete in the time they do — what is happening underneath, and why site migrations typically land in about half the wall-clock time of a single-threaded baseline, with governance scans 60–70% faster.
What a shorter migration window means for you
The user-visible difference between a long migration and a short one is almost never about throughput numbers. It is about:
- Less downtime on cutover weekend. A mid-size SharePoint site that used to take the better part of a Sunday can land in a single evening. That is a real difference for a small IT team that does not want to pull a second overnight.
- Lower risk that the window overruns. The longer a migration runs, the more chances there are to hit throttling, transient errors, or an admin who needs to step away. Shorter windows mean fewer moving pieces in flight at once.
- Faster pre-flight feedback. Governance scans and readiness checks that finish in under two minutes change how consultants work. You can iterate on a scope decision the same hour you have the meeting, not the next day.
- More migrations per weekend. For partners running multiple client cutovers, halving the per-site time doubles the number of tenants you can move in the same window.
- Users back in productivity sooner. Every hour the destination is locked down is an hour of lost billable time. Shorter migrations mean the business gets its tools back faster.
The headline number matters less than the calendar math. “Half the wall-clock time” is the difference between a cutover ending at 2am and a cutover ending at midnight. For the admin on call, those two hours are not equivalent.
Why site migrations spend most of their time on small requests
To explain why MigrationFox is fast, it helps to be honest about where the time actually goes. For file-heavy migrations (SMB-to-Blob, large document libraries) the bottleneck is bytes per second — straightforward network transfer speed. For SharePoint site migrations it is only half the story.
A typical SharePoint site migration includes:
- Enumerating site columns, content types, lists, views, and pages — metadata reads
- Creating schema on the destination — dozens of small writes per list
- Writing list items — thousands of small POSTs, each carrying a metadata payload
- Uploading files — a mix of large and small PUTs
- Replaying permissions — another burst of small writes
Most of the wall-clock time is spent on small requests, not on file bytes. A 5 GB document library can finish the byte transfer in five minutes but still spend twenty-five minutes waiting on 12,000 sequential Graph calls to tag each file with its content type and custom columns. Migrations run faster because the product optimizes the small-request path — HTTP round trips, serialization, ordering — not just the bytes-per-second number.
Graph $batch for the metadata path
Microsoft Graph’s /$batch endpoint accepts up to 20 sub-requests in a single HTTP round trip. For metadata-heavy phases — reading content types on a site, or applying column metadata across a list — that is a 20x reduction in network round trips.
Architecturally, content-type, site-column, and list-view enumeration phases are batched in groups of 20. On a site with 40 lists, that drops 120 round trips to 6. Each Graph round trip is typically 120–400ms, so the wall-clock difference on a mid-size site is roughly 30 to 45 seconds of dead air the customer does not have to sit through — multiplied across every phase of the migration.
Item writes follow a different pattern. Item creation in Graph has enough quirks (per-item field-stripping for invalid columns, per-item retry on 429, progressive fallback when a field fails) that $batch’s sub-request semantics (partial success, shared throttling bucket) are more hindrance than help. Writes take a different route.
Bounded parallelism with back-pressure
There are two naive approaches to parallel item writes. Fully sequential is safe and slow. Fully parallel with Promise.all is fast and prone to 429 storms. Neither is what a production migration needs. The pattern MigrationFox uses is bounded concurrency with back-pressure.
Every parallel phase runs with per-phase concurrency caps:
- Site enumeration: 6 lists walked in parallel, 10 pages in parallel per list
- List-item writes: 8 items in parallel per list, 4 lists in parallel per site
- File uploads: 4 blocks in parallel per file, 6 files in parallel per library
- Permissions replay: 4 in parallel (permission writes are throttle-sensitive)
On top of the cap, the runner watches the Retry-After header from every 429. If throttling kicks in, the affected phase pulls its effective concurrency down and waits for the retry window before resuming. If the tenant is healthy, the cap stays at the configured value. No manual tuning.
The subtle benefit is not top-end speed — it is consistency. Fully-parallel implementations hit 429 storms that take 30–90 seconds to drain; bounded-parallel implementations barely see 429s at all. Migrations finish faster on average because they do not blow themselves up and have to back off.
Connection reuse and HTTP/2
Every HTTP library has a default connection pool. Default agents often create a new TCP + TLS connection per request unless keep-alive is explicitly enabled, and even then the pool defaults are modest. For a process making thousands of requests to graph.microsoft.com in quick succession, that TLS handshake overhead adds up fast.
MigrationFox’s Graph client runs with:
- Persistent connection pools scoped per destination host (one pool for Graph, one for each SharePoint REST endpoint, one for each Azure Blob account)
- Keep-alive on by default with a 60-second idle timeout
- HTTP/2 multiplexing where the origin supports it — Graph does, so dozens of concurrent requests share a single connection
- Pipelining cap of 10 per connection to avoid head-of-line blocking
On a 5,000-item list, connection reuse accounts for roughly 25% of the phase wall-clock time. The first couple of requests still pay TCP+TLS, but everything after that reuses the established connection. Across a 40-minute site migration, that is meaningful time off the cutover window.
Request caching in the governance scanner
This one lives in the Copilot Readiness scanner rather than the migration path, but it is the same pattern: remove redundant Graph calls.
A governance scan runs six modules (Purview, Identity, SharePoint, Teams, OneDrive, Power Platform). The SharePoint module needs the sites list. The Teams module needs the sites list. The Identity and OneDrive modules each need the users list. A request cache scoped to a single scan run sits in front of the Graph client, keyed on the normalized URL. The first module to ask for /users?$select=id,userPrincipalName pays the API cost; every subsequent module gets the cached response. No Graph calls were removed from scan logic; they just stopped happening two or three times.
On a 1,200-user tenant, a full six-module scan completes in about 1m50s instead of 5m30s. The cache is discarded at the end of each scan, so the next run still reflects live tenant state.
The long tail of smaller optimizations
Not every architectural choice deserves its own section. A few worth mentioning because they are generally useful patterns:
- Progressive field stripping on SP item writes. If a POST fails with
invalidRequeston a read-only or computed field, the write retries once without that field instead of failing the whole item. Necessary to handle Graph’s inconsistent treatment of system columns. - No re-enumeration on retry. Retries target only the failed items; the enumeration result is cached per job.
- Streaming
drive/itemstraversal. Folder walks stream rather than loading the full tree into memory, so a million-item library does not run the runner out of memory. - Batched permission reads per list. One
$batchper 20 items instead of one read per item. - Deferred index-column creation. Lists are created with all columns first, then indexed columns are added asynchronously so schema creation does not block.
- Tight default-view payloads. The
fieldsselector requests only what the migration phase needs, not 40+ unused properties. - HEAD-after-PUT verification at commit time. On the Azure Blob path, verification happens at commit rather than per block, saving a per-block round trip.
- Delta runs skip unchanged items. The delta comparator uses Graph
cTagwhere available and falls back tomodifiedDateTimeonly whencTagis missing.
None of these is dramatic in isolation. Together they are the difference between a migration that finishes sometime tonight and one that is done by the time you get back from lunch.
What this looks like on real tenants
On three tenants where reliable timing data is available, typical migration windows look like this:
| Workload | Wall-clock time |
|---|---|
| Mid-size SP site (40 lists, 12k items, 8 GB) | ~52m |
| Small SP site (6 lists, 800 items, 400 MB) | ~4m 10s |
| Governance full scan (1,200 users) | ~1m 50s |
| Azure Blob ingest (SMB, 1 TB) | ~3h 15m |
The Azure Blob ingest number is dominated by the bytes-per-second of the network link, not per-request overhead, so the same architectural patterns matter less there. Architecture helps less when the wall clock is already set by hardware.
On Graph-based site migration, the per-request cost is the limiting factor, and that is where the architecture earns its keep. Migrations complete in about half the time they would on a single-threaded transfer.
Patterns that do not help
A few approaches look good on paper but do not deliver:
- Global retry fan-out. Letting each failing request retry aggressively across multiple connections produces worse 429 storms, not better throughput.
- Larger upload blocks. Going from 8 MB to 32 MB per block does not measurably help on a single-connection path; it just increases memory pressure. 8 MB is the sweet spot.
- Compressing request bodies. Graph does not meaningfully benefit. JSON compresses poorly on small payloads and Graph does not accept
Content-Encoding: gzipon most write endpoints. - Swapping HTTP clients indiscriminately. Alternative clients are not meaningfully faster once pool tuning is applied, and each introduces its own quirks.
Related reading
- SharePoint Site Migration platform page
- Migrating SharePoint Site Pages: why canvas layout matters
- Cross-tenant SharePoint permissions without user-mapping hell
- What’s new in Copilot Readiness (April 2026) — the governance-side request caching
Get started
Every workspace runs on this fast path by default — nothing to configure. Start a free SharePoint migration at app.migrationfox.com/register and watch your first pre-flight report come back in seconds rather than minutes.