The SharePoint Migration Cutover Playbook: Step by Step

Series: SharePoint 2019 → Subscription Edition Migration · Post 8 of 12
Reading time: ~15 minutes
Audience: SharePoint Administrators, Migration Project Managers, CAB members


It is 11 pm. The maintenance window opened two minutes ago. Your team is on the call — DBA on the SQL console, infrastructure lead at the DNS panel, project manager watching the clock. Weeks of log shipping, parallel migration runs, and dry-run rehearsals have brought you to this moment.

What separates a clean cutover from a three-hour incident isn’t luck or experience — it’s sequence. In the next 60 minutes, the order in which you execute each step determines whether your organisation wakes up on SharePoint Subscription Edition or is still troubleshooting at sunrise.

This post is your printed playbook. By the time you reach the end, you will have a complete, timed, gate-checked execution timeline — the exact sequence to follow from T-60 minutes to T+60, with PowerShell at every critical step and an explicit decision table for the moment something goes wrong.

Keep it open on a second screen. Print it if you have to. This is what the window looks like when you’re prepared.


What a Migration Cutover Actually Is

The Database-Attach Cutover Model

A SharePoint 2019 → SPSE migration uses the database-attach cutover model. You are not performing an in-place upgrade. The content databases on your SP2019 farm have been replicated to a new SQL instance via log shipping (covered in Post #6) and have been sitting in NO-RECOVERY state — receiving log backups but not yet accessible to SharePoint.

The cutover is the moment you promote those databases from NO-RECOVERY to live, attach them to SPSE web applications, and redirect users to the new farm. There is a downtime window — typically 30–90 minutes depending on database count and size. There is no zero-downtime path with this architecture. Accepting that early removes the temptation to cut corners.

Why Sequence and Timing Matter

Sequence failures are the most common cause of cutover disasters, and they are rarely obvious in the moment. Mount content databases before bringing them out of NO-RECOVERY and you will get empty site collections with no error message — just blank pages. Fail to lock the source farm to read-only before the final log apply and late writes on SP2019 will be silently orphaned. Miss the read-write unlock on SPSE after mounting and users will call the help desk reporting a “read-only SharePoint.”

The timeline in this post is the sequence that avoids all of these. Each step exists because skipping it or reordering it has caused real incidents in the field.


Pre-Cutover Checklist — The Five Gates Before T+0

All five gates must pass before the maintenance window opens. If any gate fails, postpone. A delayed cutover is recoverable. A failed cutover mid-window is not.

GateHow to VerifyPass CriteriaOwner
1. Log shipping latency < 5 minQuery the log shipping monitor job on the secondary SQL instance, or run SELECT * FROM msdb.dbo.log_shipping_monitor_secondaryAll content databases show restore latency < 5 minutesDBA
2. SP2019 DB schema ≥ 16.0.4351.1000Run SELECT * FROM [WSS_Content].dbo.Versions ORDER BY VersionId DESC on each content DB — the top row must show version ≥ 16.0.4351.1000All content databases at required schema version (Microsoft blocks upgrade below this level)DBA
3. User notification sent and confirmedConfirm maintenance notice was sent via email and SharePoint banner at least 24 hours prior; confirm help desk is on-callAcknowledgement from communications team; help desk staffed or on standbyPM
4. Rollback plan tested and readySet-SitesReadWrite.ps1 tested in staging; source farm confirmed in known-good state; DNS TTL already lowered to 60–300 secondsRollback sequence documented, all team members briefed on decision criteriaAdmin / PM
5. CAB approval obtainedCheck change management system for approved statusChange record shows “Approved”; emergency change number documented in case of rollbackPM

Gate 1 explained in numbers. If your log shipping secondary is 12 minutes behind when you begin the final restore, you have 12 minutes of user activity that will not exist on SPSE. For an active document management farm, that can be hundreds of file versions. The < 5-minute threshold is not bureaucratic — it is the maximum acceptable data exposure window.


The Cutover Timeline — T-60 Through T+60

This is the centrepiece of the playbook. Every row is an action, an owner, a command or script, an expected duration, and a gate you must confirm before moving forward. Work through it row by row. Do not skip gates.

TimePhaseActionScript / CommandOwnerDurationGate / Checkpoint
T-60Pre-checkVerify log shipping latency on all content databasesSELECT from log_shipping_monitor_secondaryDBA10 minAll DBs < 5 min latency — must pass
T-30Pre-checkConfirm CAB approval, team assembled, comms sentChange record reviewPM5 minAll four gates passed
T+0Window openOpen maintenance window; notify help desk and stakeholdersEmail / Teams messagePM2 minComms sent; help desk standing by
T+5Lock sourceSet all SP2019 site collections to read-onlySet-SitesReadOnly.ps1Admin5–10 minAll sites locked; zero write errors in ULS
T+10Disable source DBsDisable content databases on SP2019 web applicationsSet-ContentDatabasesDisabled.ps1Admin2–3 minDatabases show Disabled status in Central Administration
T+15Final syncStop log shipping jobs on source SQL; confirm final log backup deliveredEXEC msdb.dbo.sp_stop_job on log shipping backup jobDBA5 minBackup jobs stopped; final .trn file confirmed on share
T+20Final log applyApply the last log backup on the secondary; confirm LSN matchesManual RESTORE LOG or wait for final monitor cycleDBA5 minLSN on secondary matches source LSN
T+25Promote DBsBring log-shipped databases out of NO-RECOVERY into RECOVERYBring-DatabasesOnline.ps1DBA5–10 minAll databases show ONLINE in SQL; zero recovery errors
T+35Mount to SPSEMount content databases to SPSE web applicationsMount-ContentDatabases.ps1Admin5–10 minDatabases mounted; site collections visible in CA
T+45Smoke testVerify representative sites load; check critical paths (intranet, HR, document libraries)Manual — open 5–10 key URLsAdmin / PM5 minGO / NO-GO decision point
T+50Update DNSPoint DNS or load balancer to SPSE farmDNS console / load balancer configInfra5 minDNS resolving to SPSE (verify with nslookup)
T+55Post-DNS smoke testRe-run site checks after DNS cut; confirm users can reach SPSEManualAdmin5 minAll critical sites load; no auth errors
T+57Confirm source disabledDisable source content databases if not already doneSet-ContentDatabasesDisabled.ps1 (verify)Admin2 minSource shows Disabled; no writes possible
T+60Declare successSet SPSE sites to read-write; notify stakeholders; close change recordSet-SitesReadWrite.ps1Admin / PM2–5 minSites read-write; stakeholders notified; ITSM ticket closed

Scaling note: This timeline assumes 5–8 content databases of moderate size (< 100 GB each). Add approximately 10 minutes per additional 5 databases for mount operations. Plan your window accordingly during the change request.

The GO / NO-GO Moment

The T+45 smoke test is the most important gate in the entire timeline. At this point, databases are mounted and sites are accessible — but DNS has not yet been updated and the source farm is still intact. This is the last clean opportunity to call a rollback before users are affected.

At T+45 you are asking one question: Do the right things work? Not “is everything perfect” — you are confirming that critical business sites load, document libraries are accessible, and there are no farm-level errors. If three minor sites have issues, that is worth investigating. If the intranet home page fails to load or a file download returns an error, you have a mounting problem, not a site problem. Call NO-GO immediately.

If the smoke test passes, call GO and proceed to DNS. The window from T+45 to T+50 is your last quiet moment of the night.

The full Cutover Execution Bundle includes all five scripts below in production-ready form — parameter-validated, with dry-run -WhatIf support, error handling, and per-script READMEs. If you would rather not build these from scratch at 11 pm, that is what the bundle is for.


Key Script Breakdown — What Each Script Does

Set-SitesReadOnly.ps1 — Locking the Source Farm (T+5)

The read-only lock is the first action you take after the window opens. Any write that occurs on the source farm after the final log apply will not exist on SPSE — you need the lock in place before the final restore begins, not after.

SPSite.LockState = ReadOnly prevents new content writes while still allowing users to read existing content. This is less disruptive than NoAccess (which blocks all access) and is the correct choice for the cutover window.

# Set-SitesReadOnly.ps1 — run on the SP2019 farm
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
$webAppUrls = @(
"https://sharepoint.contoso.com",
"https://mysite.contoso.com"
)
foreach ($url in $webAppUrls) {
$webApp = Get-SPWebApplication -Identity $url
foreach ($site in $webApp.Sites) {
Write-Host "Locking: $($site.Url)"
$site.LockState = [Microsoft.SharePoint.SPSiteLockState]::ReadOnly
$site.Dispose()
}
Write-Host "Web application $url — all sites set to ReadOnly."
}

After running: Verify by browsing to a site collection and confirming the read-only banner appears. Check ULS logs for any lock-setting errors.


Set-ContentDatabasesDisabled.ps1 — Belt-and-Suspenders Source Lock (T+10)

Even with sites in read-only state, background service application jobs can attempt to write to content databases. Disabling the databases at the farm level in Central Administration closes this gap entirely.

# Set-ContentDatabasesDisabled.ps1 — run on the SP2019 farm
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
$webAppUrl = "https://sharepoint.contoso.com"
$webApp = Get-SPWebApplication -Identity $webAppUrl
foreach ($contentDb in $webApp.ContentDatabases) {
Write-Host "Disabling database: $($contentDb.Name)"
Set-SPContentDatabase -Identity $contentDb.Name -Status Disabled
}
Write-Host "All content databases on $webAppUrl set to Disabled."

After running: Open Central Administration → Manage Content Databases. Confirm all databases show Disabled status. This is your visible confirmation that the source farm is fully locked before you touch the secondary SQL.


Bring-DatabasesOnline.ps1 — Promoting from NO-RECOVERY (T+25)

This is the most technically critical step in the entire cutover. A database in NO-RECOVERY state is receiving log backups but is not accessible — think of it as a box of files still being unpacked. RESTORE DATABASE ... WITH RECOVERY closes the box and promotes it to a live, queryable state. Only after this step can SharePoint mount the database.

Order matters: all log backups must be applied before you run WITH RECOVERY. Running this script before the final log apply (T+20) will leave you with databases that are online but missing the last few minutes of data.

# Bring-DatabasesOnline.ps1 — run on the SPSE SQL instance
$sqlInstance = "SPSE-SQL01"
$databases = @(
"SP_Content_Intranet",
"SP_Content_MySites",
"SP_Content_Portal"
)
foreach ($db in $databases) {
Write-Host "Promoting $db to RECOVERY state..."
try {
Invoke-Sqlcmd -ServerInstance $sqlInstance `
-Query "RESTORE DATABASE [$db] WITH RECOVERY" `
-QueryTimeout 300
Write-Host "SUCCESS: $db is now ONLINE." -ForegroundColor Green
}
catch {
Write-Host "ERROR: Failed to recover $db — $($_.Exception.Message)" -ForegroundColor Red
}
}

After running: In SSMS, verify each database shows Online status (green circle). Any database still in Restoring state means the final log backup was not applied — do not proceed to mount until resolved.


Mount-ContentDatabases.ps1 — Attaching Databases to SPSE (T+35)

Mount-SPContentDatabase is the SharePoint command that associates a recovered SQL database with an SPSE web application, registers all site collections it finds, and makes them accessible to users. The database must be in ONLINE state (previous step) before this will work.

# Mount-ContentDatabases.ps1 — run on the SPSE farm
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
$databaseServer = "SPSE-SQL01"
$webAppUrl = "https://spse.contoso.com"
$databases = @(
"SP_Content_Intranet",
"SP_Content_MySites",
"SP_Content_Portal"
)
foreach ($dbName in $databases) {
Write-Host "Mounting $dbName to $webAppUrl..."
try {
Mount-SPContentDatabase `
-Name $dbName `
-DatabaseServer $databaseServer `
-WebApplication $webAppUrl `
-Confirm:$false
Write-Host "SUCCESS: $dbName mounted." -ForegroundColor Green
}
catch {
Write-Host "ERROR: Failed to mount $dbName — $($_.Exception.Message)" -ForegroundColor Red
}
}

After running: In Central Administration → Content Databases, confirm all databases show Ready status. Browse to two or three site collections to confirm they load. If any database mount throws an error, note which one — this is your primary GO/NO-GO data point.


Set-SitesReadWrite.ps1 — Releasing the Lock (T+60)

This script is used in two scenarios: after a successful cutover to release the read-only state inherited by SPSE databases from the source farm, and on SP2019 during a rollback to restore user access.

Important: Run this on the SPSE farm after cutover. When databases are mounted from a log-shipped secondary, they inherit the ReadOnly lock state from the source. Failing to run this script is one of the most common post-cutover issues — users will report that everything is read-only even though the farm appears healthy.

# Set-SitesReadWrite.ps1 — run on the SPSE farm after successful cutover
# (or on SP2019 during rollback)
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
$webAppUrls = @(
"https://spse.contoso.com",
"https://mysite-spse.contoso.com"
)
foreach ($url in $webAppUrls) {
$webApp = Get-SPWebApplication -Identity $url
foreach ($site in $webApp.Sites) {
Write-Host "Unlocking: $($site.Url)"
$site.LockState = [Microsoft.SharePoint.SPSiteLockState]::Unlock
$site.Dispose()
}
Write-Host "Web application $url — all sites set to Unlock (read-write)."
}

After running: Browse to a previously read-only site. Confirm the read-only banner is gone and that a document library allows file uploads.


The Rollback Plan — When and How to Abort

The rollback plan is not a footnote. It is a tested, documented sequence that your team rehearses before the window opens. If something goes wrong, you should not be making up the recovery steps under pressure.

Rollback Trigger Criteria

Use this table to make the GO/NO-GO decision mechanical. You should not be debating subjectively at T+45 — you should be checking a table.

ScenarioTrigger ConditionDecision
Bring-DatabasesOnline.ps1 fails on one or more databasesRecovery errors in script output or database still shows Restoring in SSMSRollback immediately — do not proceed to mount
Mount-ContentDatabases.ps1 errors on 20%+ of databasesError output in script; databases not visible in CARollback — partial mount leaves farm in inconsistent state
Critical sites fail smoke test (intranet, HR, legal portal)T+45 smoke test: site returns 503 or blank contentRollback — farm-level issue, not site-level
< 10% of sites have issues at smoke testOnly minor or peripheral sites affectedInvestigate first — may be site-level, not a mount failure
DNS propagation taking > 20 minutesT+55 nslookup still returns old IPHold — extend window, do not rollback yet
Database corruption error during RECOVERY promotionSQL error 824/823 or torn page error in recovery outputRollback immediately, escalate to DBA lead

Rollback Sequence — Step by Step

  1. Call NO-GO at the T+45 gate (or immediately at any earlier failure point). PM confirms rollback decision on the team call. Log the time.
  2. Reverse DNS — update DNS or load balancer to point back to the SP2019 farm. With a lowered TTL (60–300 seconds), this propagates quickly.
  3. Re-enable content databases on SP2019 — reverse the Set-ContentDatabasesDisabled.ps1 step. In Central Administration, set each database back to Ready status, or run: Set-SPContentDatabase -Identity "SP_Content_Intranet" -Status Online
  4. Release read-only lock on SP2019 — run Set-SitesReadWrite.ps1 against the SP2019 web applications. Verify users can write to documents.
  5. Verify SP2019 accessibility — manually browse 3–5 site collections, confirm file access, open and edit a document.
  6. Disable SPSE content databases (if mounted) — run Set-ContentDatabasesDisabled.ps1 on the SPSE farm to prevent any accidental traffic from writing to them during the post-rollback period.
  7. Notify all stakeholders — PM sends rollback notification with the new maintenance window estimate. Help desk notifies any users who were active during the window.
  8. Open an incident/change record — log the rollback in your ITSM system with the exact failure point, time, and DBA/admin notes.
  9. Post-mortem before re-attempting — identify the root cause (log shipping gap, SQL recovery failure, mount error, DNS issue) and address it completely before scheduling the next attempt.

What Rollback Does Not Recover

Be clear about this with your team before the window opens: rollback is not a perfect reset.

If the read-only lock on SP2019 was slow to apply and any user managed to write content in the gap between T+5 and T+20, that content exists on SP2019 but was not captured in the final log backup applied to SPSE. You will not lose it — it is on the source — but it would not have been on SPSE if the cutover had succeeded.

This is the primary reason the read-only lock step must complete fully with zero errors before any log shipping jobs are touched. The sequence protects the data. Rushing the sequence is where data exposure actually happens.

SPSE databases in NO-RECOVERY state can remain as-is for the next cutover attempt. They do not need to be re-initialised unless a corruption event occurred during the failed recovery. Your DBA should confirm this before the next window.


Post-Cutover Immediate Actions (T+60 to T+120)

Closing the maintenance window is not the end of the work. The first hour after declaring success is when silent failures surface. Work through this list methodically before standing down the team.

  1. Run Set-SitesReadWrite.ps1 on SPSE — confirm this is done before you end the team call. It is the most commonly missed post-cutover step.
  2. Start a full search crawl on SPSE — the Search Service Application index is empty or stale. Open Central Administration → Search Service Application → Content Sources and start a full crawl immediately. It will run in the background; results will not be instant.
  3. Verify Managed Metadata Service — confirm the Managed Metadata Service Application is running and term store connections are visible in at least one site collection.
  4. Check User Profile Service sync — verify the UPS import is scheduled or trigger a full import if user profiles were migrated separately.
  5. Spot-check ULS logs for critical errors — on the SPSE application servers, tail the ULS log for 15 minutes and look for any CriticalUnhandledException or Unexpected entries related to content database access or authentication.
  6. Validate authentication — have a user outside the admin team log in from a standard workstation and confirm they reach the correct farm with their identity intact.
  7. Notify stakeholders — PM sends the “migration complete” notice with the actual downtime duration and help desk contact details for any issues. Set the expectation that search will be rebuilding for several hours.
  8. Close the change record — update the ITSM ticket with actual start time, actual completion time, any deviations from plan, and final status. This closes the CAB loop.

Run Your SharePoint Cutover With Confidence

You have one shot at the cutover window. Don’t improvise the scripts.

The timeline table in this post tells you what to run and when. The Cutover Execution Bundle gives you the scripts that are tested, documented, and ready to execute when the window opens — written to run reliably at midnight, not just in a lab.

The bundle includes all five scripts from this playbook:

ScriptWhat It Does
Set-SitesReadOnly.ps1Locks all site collections across specified web applications at the read-only state
Set-ContentDatabasesDisabled.ps1Disables content databases on the source farm to prevent background writes during cutover
Bring-DatabasesOnline.ps1Promotes log-shipped databases from NO-RECOVERY to RECOVERY state on the SPSE SQL instance
Mount-ContentDatabases.ps1Mounts restored content databases to SPSE web applications using Mount-SPContentDatabase
Set-SitesReadWrite.ps1Releases the read-only lock post-cutover (and on the source during rollback)

Every script includes full parameter validation, -WhatIf dry-run support, structured error handling, and a per-script README with the exact invocation pattern from this timeline.

Contact sudharsan_1985@live.in to get the Cutover Execution Bundle.

Not ready yet? Start from the beginning with Post #1: The Complete SharePoint 2019 → SPSE Migration Guide, or get everything at once with the Complete SP2019 → SPSE Migration Toolkit — contact the same address for details.


Conclusion

A cutover succeeds or fails on sequence discipline. Not on the quality of the farm, not on the speed of the server, not on the experience of the team in the room — on whether the right steps happen in the right order with the right gate checks between them. The timeline table in this post is that discipline made explicit.

You now have the sequence, the gates, the scripts, and the rollback plan. The rest is execution.

If you are running SPSE in a production environment and want to make sure it survives the load it just inherited, the next step is hardening the SQL layer. Post #9: Adding Content Databases to a SQL Server Availability Group covers exactly that — the first post-cutover step toward high availability on SPSE.

You did the work. Now go run a clean cutover.


Post 8 of 12 in the SharePoint 2019 → Subscription Edition Migration series.
Post #7: Parallel Migration for Large Farms · Post #9: Availability Groups on SPSE →

Leave a Reply