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.
| Gate | How to Verify | Pass Criteria | Owner |
|---|---|---|---|
| 1. Log shipping latency < 5 min | Query the log shipping monitor job on the secondary SQL instance, or run SELECT * FROM msdb.dbo.log_shipping_monitor_secondary | All content databases show restore latency < 5 minutes | DBA |
| 2. SP2019 DB schema ≥ 16.0.4351.1000 | Run SELECT * FROM [WSS_Content].dbo.Versions ORDER BY VersionId DESC on each content DB — the top row must show version ≥ 16.0.4351.1000 | All content databases at required schema version (Microsoft blocks upgrade below this level) | DBA |
| 3. User notification sent and confirmed | Confirm maintenance notice was sent via email and SharePoint banner at least 24 hours prior; confirm help desk is on-call | Acknowledgement from communications team; help desk staffed or on standby | PM |
| 4. Rollback plan tested and ready | Set-SitesReadWrite.ps1 tested in staging; source farm confirmed in known-good state; DNS TTL already lowered to 60–300 seconds | Rollback sequence documented, all team members briefed on decision criteria | Admin / PM |
| 5. CAB approval obtained | Check change management system for approved status | Change record shows “Approved”; emergency change number documented in case of rollback | PM |
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.
| Time | Phase | Action | Script / Command | Owner | Duration | Gate / Checkpoint |
|---|---|---|---|---|---|---|
| T-60 | Pre-check | Verify log shipping latency on all content databases | SELECT from log_shipping_monitor_secondary | DBA | 10 min | All DBs < 5 min latency — must pass |
| T-30 | Pre-check | Confirm CAB approval, team assembled, comms sent | Change record review | PM | 5 min | All four gates passed |
| T+0 | Window open | Open maintenance window; notify help desk and stakeholders | Email / Teams message | PM | 2 min | Comms sent; help desk standing by |
| T+5 | Lock source | Set all SP2019 site collections to read-only | Set-SitesReadOnly.ps1 | Admin | 5–10 min | All sites locked; zero write errors in ULS |
| T+10 | Disable source DBs | Disable content databases on SP2019 web applications | Set-ContentDatabasesDisabled.ps1 | Admin | 2–3 min | Databases show Disabled status in Central Administration |
| T+15 | Final sync | Stop log shipping jobs on source SQL; confirm final log backup delivered | EXEC msdb.dbo.sp_stop_job on log shipping backup job | DBA | 5 min | Backup jobs stopped; final .trn file confirmed on share |
| T+20 | Final log apply | Apply the last log backup on the secondary; confirm LSN matches | Manual RESTORE LOG or wait for final monitor cycle | DBA | 5 min | LSN on secondary matches source LSN |
| T+25 | Promote DBs | Bring log-shipped databases out of NO-RECOVERY into RECOVERY | Bring-DatabasesOnline.ps1 | DBA | 5–10 min | All databases show ONLINE in SQL; zero recovery errors |
| T+35 | Mount to SPSE | Mount content databases to SPSE web applications | Mount-ContentDatabases.ps1 | Admin | 5–10 min | Databases mounted; site collections visible in CA |
| T+45 | Smoke test | Verify representative sites load; check critical paths (intranet, HR, document libraries) | Manual — open 5–10 key URLs | Admin / PM | 5 min | GO / NO-GO decision point |
| T+50 | Update DNS | Point DNS or load balancer to SPSE farm | DNS console / load balancer config | Infra | 5 min | DNS resolving to SPSE (verify with nslookup) |
| T+55 | Post-DNS smoke test | Re-run site checks after DNS cut; confirm users can reach SPSE | Manual | Admin | 5 min | All critical sites load; no auth errors |
| T+57 | Confirm source disabled | Disable source content databases if not already done | Set-ContentDatabasesDisabled.ps1 (verify) | Admin | 2 min | Source shows Disabled; no writes possible |
| T+60 | Declare success | Set SPSE sites to read-write; notify stakeholders; close change record | Set-SitesReadWrite.ps1 | Admin / PM | 2–5 min | Sites 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
-WhatIfsupport, 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 farmAdd-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 farmAdd-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue$webAppUrl = "https://sharepoint.contoso.com"$webApp = Get-SPWebApplication -Identity $webAppUrlforeach ($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 farmAdd-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.
| Scenario | Trigger Condition | Decision |
|---|---|---|
Bring-DatabasesOnline.ps1 fails on one or more databases | Recovery errors in script output or database still shows Restoring in SSMS | Rollback immediately — do not proceed to mount |
Mount-ContentDatabases.ps1 errors on 20%+ of databases | Error output in script; databases not visible in CA | Rollback — 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 content | Rollback — farm-level issue, not site-level |
| < 10% of sites have issues at smoke test | Only minor or peripheral sites affected | Investigate first — may be site-level, not a mount failure |
| DNS propagation taking > 20 minutes | T+55 nslookup still returns old IP | Hold — extend window, do not rollback yet |
| Database corruption error during RECOVERY promotion | SQL error 824/823 or torn page error in recovery output | Rollback immediately, escalate to DBA lead |
Rollback Sequence — Step by Step
- 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.
- Reverse DNS — update DNS or load balancer to point back to the SP2019 farm. With a lowered TTL (60–300 seconds), this propagates quickly.
- Re-enable content databases on SP2019 — reverse the
Set-ContentDatabasesDisabled.ps1step. In Central Administration, set each database back to Ready status, or run:Set-SPContentDatabase -Identity "SP_Content_Intranet" -Status Online - Release read-only lock on SP2019 — run
Set-SitesReadWrite.ps1against the SP2019 web applications. Verify users can write to documents. - Verify SP2019 accessibility — manually browse 3–5 site collections, confirm file access, open and edit a document.
- Disable SPSE content databases (if mounted) — run
Set-ContentDatabasesDisabled.ps1on the SPSE farm to prevent any accidental traffic from writing to them during the post-rollback period. - Notify all stakeholders — PM sends rollback notification with the new maintenance window estimate. Help desk notifies any users who were active during the window.
- Open an incident/change record — log the rollback in your ITSM system with the exact failure point, time, and DBA/admin notes.
- 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.
- Run
Set-SitesReadWrite.ps1on SPSE — confirm this is done before you end the team call. It is the most commonly missed post-cutover step. - 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.
- Verify Managed Metadata Service — confirm the Managed Metadata Service Application is running and term store connections are visible in at least one site collection.
- Check User Profile Service sync — verify the UPS import is scheduled or trigger a full import if user profiles were migrated separately.
- Spot-check ULS logs for critical errors — on the SPSE application servers, tail the ULS log for 15 minutes and look for any
CriticalUnhandledExceptionorUnexpectedentries related to content database access or authentication. - 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.
- 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.
- 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:
| Script | What It Does |
|---|---|
Set-SitesReadOnly.ps1 | Locks all site collections across specified web applications at the read-only state |
Set-ContentDatabasesDisabled.ps1 | Disables content databases on the source farm to prevent background writes during cutover |
Bring-DatabasesOnline.ps1 | Promotes log-shipped databases from NO-RECOVERY to RECOVERY state on the SPSE SQL instance |
Mount-ContentDatabases.ps1 | Mounts restored content databases to SPSE web applications using Mount-SPContentDatabase |
Set-SitesReadWrite.ps1 | Releases 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 →