Support for SharePoint Server 2016 and 2019 ended on 14 July 2026, and the only supported on-premises destination is Subscription Edition. This article is the sequence, in PowerShell, with the parts that block a migration marked where they actually bite.
One thing before the code, because it changes the plan and the budget.
SharePoint 2016 goes straight to Subscription Edition
The most expensive belief in this project is that you have to pass through 2019 first. You do not.
"SharePoint Server Subscription Edition supports both N - 1 and N - 2 version-to-version upgrade. You can upgrade directly from the following SharePoint products using the standard database attach upgrade procedure: SharePoint Server 2019 (including Project Server 2019), SharePoint Server 2016 (including Project Server 2016)."
One hop, from either. The double hop is only required below 2016:
"Directly upgrading from versions of SharePoint earlier than SharePoint Server 2016 via database attach is not supported. SharePoint 2013, SharePoint 2010, and so on must first be upgraded to either SharePoint Server 2016 or SharePoint Server 2019 via database attach before upgrading to SharePoint Server Subscription Edition."

And there is a floor underneath all of it:
"All databases must be upgraded to version 16.0.4351.1000 or higher, otherwise upgrade to SharePoint Server Subscription Edition will be blocked."
That number is the SharePoint Server 2016 RTM build. Inspect the actual database upgrade state; the product label alone is not evidence that every upgrade completed.
The method decides the plan
There is no in-place upgrade. The only supported route is database attach, which means you build a second farm and move content into it. Configuration does not travel: web applications, service applications, policies and settings are rebuilt by hand in the new farm. That is not a limitation to work around, it is the shape of the project.

Microsoft documents the upgrade in four phases: create the Subscription Edition farm, copy the databases, upgrade the service applications, then upgrade the content databases. Its page on creating the farm keeps the web applications out of the first phase:
"In a standard installation, the next step would be to create web applications. However, for upgrade, you create web applications later in the process, after you upgrade the service application databases."
The content phase opens with them and with the customisations, before any content database is attached. That is Microsoft's documented order, and the phases below follow it. Microsoft's upgrade pages do not say that creating the web applications or deploying the solutions earlier is unsupported, and they do not describe doing it either. This article splits Microsoft's last phase in two: phase 4 creates the web applications and deploys the customisations, and phase 5 tests and mounts the content databases.
Everything below assumes the SharePoint Management Shell running as an elevated administrator, with the account holding db_owner on every database being upgraded, securityadmin on the SQL instance, membership of the local Administrators group, and rights granted through Add-SPShellAdmin.
Rehearse this before you run it. Every cmdlet and every parameter below comes from Microsoft's documentation, and the order is the one Microsoft documents, except where a phase says it differs. But no two farms are the same, and a sequence assembled from documentation is a plan, not a script to paste into production. Not every property the scripts read is documented, either: the comments mark the ones known not to be, and the rehearsal is where they are confirmed. Build it on a test farm restored from your own backups, time each phase there, and only then run it for real. The rehearsal section at the end is not optional advice.
Phase 0: the inventory, taken before anything moves
The new farm has to be able to answer for everything the old one had. Take the inventory first, keep it in source control, and diff against it at the end. This runs on the old farm.
# INVENTORY.PS1: runs on the SharePoint 2016 or 2019 farm.
# Reads only. Produces the record that the new farm gets compared against.
$out = "C:\Migration\inventory"
New-Item -ItemType Directory -Path $out -Force | Out-Null
# Patch level of every SharePoint product in the farm. This is the documented
# way to see what is actually installed, server by server.
Get-SPProduct |
Select-Object ProductName, @{n='Patches';e={ $_.PatchableUnitDisplayNames -join '; ' }} |
Export-Csv "$out\products.csv" -NoTypeInformation -Encoding UTF8
# Web applications with their URLs and authentication. The new farm has to
# reproduce these URLs exactly, or every link inside the content breaks.
Get-SPWebApplication -IncludeCentralAdministration |
Select-Object DisplayName, Url,
@{n='Zones';e={ ($_.AlternateUrls | ForEach-Object { "$($_.UrlZone)=$($_.IncomingUrl)" }) -join ' | ' }},
@{n='ClaimsMode';e={ $_.UseClaimsAuthentication }} |
Export-Csv "$out\webapps.csv" -NoTypeInformation -Encoding UTF8
# Content databases, their sizes and how many site collections each holds.
# The counts are what tells you which mount will take twenty minutes and
# which will take six hours.
Get-SPContentDatabase |
Select-Object Name, Server, WebApplication, CurrentSiteCount,
@{n='SizeGB';e={ [math]::Round($_.DiskSizeRequired / 1GB, 2) }} |
Export-Csv "$out\contentdbs.csv" -NoTypeInformation -Encoding UTF8
# Farm solutions. These have to be deployed to the new farm BEFORE the content
# databases are tested, and this list is what you deploy.
Get-SPSolution |
Select-Object Name, Deployed, ContainsGlobalAssembly, ContainsWebApplicationResource,
@{n='DeployedTo';e={ ($_.DeployedWebApplications | ForEach-Object { $_.Url }) -join ' | ' }} |
Export-Csv "$out\solutions.csv" -NoTypeInformation -Encoding UTF8
# Service applications and their databases. Five of these can be upgraded,
# and a farm moving its 2013 workflows also brings App Management (phase 3).
# Everything else on this list is a rebuild, and it is better to know now.
Get-SPServiceApplication |
Select-Object DisplayName, TypeName, Status |
Export-Csv "$out\serviceapps.csv" -NoTypeInformation -Encoding UTF8
Get-SPDatabase |
Select-Object Name, Type, Server,
@{n='SizeGB';e={ [math]::Round($_.DiskSizeRequired / 1GB, 2) }} |
Export-Csv "$out\databases.csv" -NoTypeInformation -Encoding UTF8
# Every site collection, with its template. The templates are what a missing
# feature shows up as later, so this is the list you check against.
Get-SPSite -Limit All |
Select-Object Url, @{n='Template';e={ $_.RootWeb.WebTemplate + '#' + $_.RootWeb.Configuration }},
ContentDatabase, Owner |
Export-Csv "$out\sites.csv" -NoTypeInformation -Encoding UTF8
Write-Host "Inventory written to $out" -ForegroundColor GreenTwo of those files come back in phase 4. webapps.csv becomes the web applications, because the URLs in the new farm have to match the old ones character for character, and solutions.csv becomes the solutions deployed to them.
Phase 1: the farm that receives it
Build the Subscription Edition farm, and stop before the web applications: they come in phase 4. One decision belongs here, and Microsoft states it without giving a reason:
"If you want to upgrade these service applications by upgrading the service application databases, do not use the Farm Configuration Wizard to configure these service applications when you set up your new farm."
Certificates belong here too, because Subscription Edition manages them in the farm, and a web application created in phase 4 can be handed one at creation.
# CERTIFICATES. Subscription Edition deploys imported certificates to the
# Windows certificate store on every server in the farm automatically, and
# to any server that joins later.
$pfxPassword = Read-Host -AsSecureString -Prompt 'PFX password'
Import-SPCertificate -Path '\\fileserver\certs\sharepoint.contoso.com.pfx' `
-Password $pfxPassword -Exportable
# The whole chain has to be present, not just the leaf, or SSL connections fail.
Import-SPCertificate -Path '\\fileserver\certs\issuing-ca.p7b'
# Confirm what landed, and in which store.
Get-SPCertificate | Select-Object DisplayName, Store, NotAfter, Thumbprint
# Warn earlier than the default. The health rules then chase expiry for you.
Set-SPCertificateSettings -CertificateExpirationWarningThreshold 45Phase 2: freeze, copy, thaw
The old farm goes read-only rather than offline, so people keep reading while the copy runs. This is T-SQL, on the source instance.
-- ON THE OLD FARM'S SQL INSTANCE.
-- Read-only, not offline: people keep reading during the copy window.
ALTER DATABASE [WSS_Content_Intranet] SET READ_ONLY WITH ROLLBACK IMMEDIATE;
-- COPY_ONLY leaves the existing backup chain intact. Without it, this backup
-- becomes part of the chain and the scheduled restore plan quietly changes.
-- CHECKSUM makes the restore refuse a backup that was damaged in transit.
BACKUP DATABASE [WSS_Content_Intranet]
TO DISK = N'\\fileserver\migration\WSS_Content_Intranet.bak'
WITH COPY_ONLY, COMPRESSION, CHECKSUM, STATS = 5;-- ON THE SUBSCRIPTION EDITION FARM'S SQL INSTANCE.
RESTORE DATABASE [WSS_Content_Intranet]
FROM DISK = N'\\fileserver\migration\WSS_Content_Intranet.bak'
WITH MOVE 'WSS_Content_Intranet' TO N'E:\SQLData\WSS_Content_Intranet.mdf',
MOVE 'WSS_Content_Intranet_log' TO N'F:\SQLLogs\WSS_Content_Intranet_log.ldf',
REPLACE, STATS = 5;
-- A restored copy inherits the read-only state, and a read-only database
-- cannot be attached. This is the step people forget, and Mount fails on it.
ALTER DATABASE [WSS_Content_Intranet] SET READ_WRITE WITH ROLLBACK IMMEDIATE;The same three statements apply to the service application databases, with one exception handled in the next phase: the Search administration database is copied later, and only after Search has been paused. Microsoft also holds back the User Profile databases: it sets them read-only and copies them in the service-application phase, just before upgrading them. This sequence copies them here with the rest, and none of Microsoft's upgrade pages says whether that difference matters.
Phase 3: the service applications, in the order that is not a preference
Five service application databases can be upgraded: Business Data Connectivity, Managed Metadata, Secure Store, User Profile and Search. A farm that moves its SharePoint 2013 workflows also moves a sixth, App Management. Microsoft's workflow upgrade page says:
"You must move the App Management database along with the content, WFM, and Service Bus databases. If you don't, all workflows created before the migration will fail."
The same page adds that "the problem can be corrected later", and that "it is easier to avoid it by bringing the App Management database along during the upgrade/migration." Its link for that correction now leads to a retirement notice, and the correction Microsoft still documents is on its pages for upgrades to 2016 and 2019. The page's own procedure restores the database and creates a new App Management service application over it, which it says "should upgrade the database to the current SharePoint version". The rest of the workflow move, from the Workflow Manager and Service Bus databases to registering the service again, is that page's own procedure and is not scripted here. Microsoft's update notes for August, September and October 2026 each carry the same prerequisite for a farm on SharePoint Workflow Manager (October 2026 notes, KB5002927):
"If you're currently running SharePoint Workflow Manager, you must install SharePoint Workflow Manager (KB5002799) to your farm before you install this cumulative update."
Every other service is reconfigured by hand, and two are explicitly refused:
"Word Automation Services and Machine Translation Services can't be upgraded. A new service instance will need to be created."
The order is fixed by dependencies. Managed Metadata and User Profile both come before Search, and both come before any My Site.
# SERVICE APPLICATIONS. Order matters: MMS and UPA before Search.
$applicationPool = Get-SPServiceApplicationPool -Identity 'SharePoint Web Services default'
# --- Secure Store -------------------------------------------------------
# The passphrase is from the OLD farm. Without it the stored credentials are
# unreadable, and no amount of restoring gets them back.
$sss = New-SPSecureStoreServiceApplication -Name 'Secure Store' `
-ApplicationPool $applicationPool `
-DatabaseName 'Secure_Store_Service_DB' -AuditingEnabled
$sssp = New-SPSecureStoreServiceApplicationProxy -Name 'Secure Store Proxy' `
-ServiceApplication $sss -DefaultProxyGroup
# Microsoft's procedure puts one more step here: once the service application
# and the proxy exist, "you have to refresh the encryption key", which its
# Secure Store page does in Central Administration (Refresh Key, with the
# pass phrase). It does not say whether the command below is that refresh or
# a second step: settle it in the rehearsal.
$oldPassphrase = Read-Host -Prompt 'Secure Store passphrase from the old farm'
Update-SPSecureStoreApplicationServerKey -Passphrase $oldPassphrase `
-ServiceApplicationProxy $sssp
# --- Business Data Connectivity ----------------------------------------
# The only one that creates its own proxy and puts it in the default group.
New-SPBusinessDataCatalogServiceApplication -Name 'BDC Service' `
-ApplicationPool $applicationPool -DatabaseName 'BDC_Service_DB'
# --- Managed Metadata ---------------------------------------------------
$mms = New-SPMetadataServiceApplication -Name 'Managed Metadata Service' `
-ApplicationPool $applicationPool `
-DatabaseName 'Managed_Metadata_Service_DB'
New-SPMetadataServiceApplicationProxy -Name 'Managed Metadata Proxy' `
-ServiceApplication $mms -DefaultProxyGroup
# --- User Profile -------------------------------------------------------
# After Managed Metadata, and before any My Site is touched. This follows
# Microsoft's procedure, which copies only Profile and Social and names a NEW
# Sync database. Its services overview says the sync database must be
# upgraded too: the two pages conflict, as the note after this block shows.
New-SPProfileServiceApplication -Name 'User Profile Service Application' `
-ApplicationPool $applicationPool `
-ProfileDBName 'UPA_ProfileDB' `
-SocialDBName 'UPA_SocialDB' `
-ProfileSyncDBName 'UPA_SyncDB'
$upa = Get-SPServiceApplication | Where-Object { $_.TypeName -eq 'User Profile Service Application' }
New-SPProfileServiceApplicationProxy -Name 'User Profile Proxy' -ServiceApplication $upa
$upaProxy = Get-SPServiceApplicationProxy |
Where-Object { $_.TypeName -eq 'User Profile Service Application Proxy' }
Add-SPServiceApplicationProxyGroupMember -member $upaProxy -identity ''The User Profile lines take one side of a conflict between Microsoft's pages. The service-application procedure copies only the Profile and Social databases and says "SyncDBName is the name of the new Synchronization database." The services upgrade overview says "The User Profile service uses databases to store profile, social, and sync information. These databases must be upgraded as part of a services database attach upgrade." The script follows the procedure.
Search is last, and it has a step on the old farm in the middle of it.
# --- Search, part one: ON THE OLD FARM ---------------------------------
# The Search Administration database is copied later than the others, because
# Search has to be paused first. While it is paused the old index stops
# updating, so results on the old farm get staler during the window.
$ssaOld = Get-SPEnterpriseSearchServiceApplication 'Search Service Application'
Suspend-SPEnterpriseSearchServiceApplication -Identity $ssaOld
# Now set the Search Admin DB read-only, back it up and restore it, as in phase 2.
# Nothing below resumes it on this farm: see the note after part two.# --- Search, part two: ON THE SUBSCRIPTION EDITION FARM ----------------
# The Search service instance cannot be started from Central Administration
# until a Search service application exists, so PowerShell it is.
$searchInst = Get-SPEnterpriseSearchServiceInstance -local
Start-SPServiceInstance $searchInst
# Restore, not New: this is what upgrades the admin database instead of
# creating an empty one. It keeps the search schema, the result sources and
# the query rules. It does NOT keep the index.
Restore-SPEnterpriseSearchServiceApplication `
-Name 'Search Service Application' `
-applicationpool $applicationPool `
-databasename 'Search_Service_Application_DB' `
-databaseserver 'SQLSE01' `
-AdminSearchServiceInstance $searchInst
$ssa = Get-SPEnterpriseSearchServiceApplication
New-SPEnterpriseSearchServiceApplicationProxy -Name 'Search Proxy' -SearchApplication $ssa
$ssap = Get-SPEnterpriseSearchServiceApplicationProxy
Add-SPServiceApplicationProxyGroupMember -member $ssap -identity ''If that restore fails, and network or SQL latency is enough to make it fail, the documented recovery is not to retry in place: delete the half-upgraded Search Administration database, restore the backup copy again, set it read-write, and run the restore command once more.
Microsoft's procedure ends Search with one more step, "Resume the Search service application in the SharePoint Server environment", using $ssa.ForceResume(0x02). The step itself names no farm. The procedure's first step ends with "Perform the next steps in the SharePoint Server Subscription Edition environment." Step 8 then goes back to the old farm: "In the SharePoint Server 2019 or SharePoint Server 2016 farm, backup the Analytics Reporting database." Its pages point both ways. The 2016 version of the step names the source farm and uses $ssa.Resume(), while the page on paused Search service applications ties the 0x02 flag to "A backup or restore procedure is in progress." Read $ssa.IsPaused() on the new farm once the restore has completed, in the rehearsal and again on the day, and act on any flag it reports as that page describes.
On the old farm, Search stays suspended for as long as that farm is the read-only source. This sequence does not resume it while its Search Administration database is read-only; Microsoft's read-only farm page says "The Search service application does not function when its databases are set as read-only." If a rollback returns the old farm to service, set its Search Administration database back to read-write, then resume the application there with Resume-SPEnterpriseSearchServiceApplication, the cmdlet Microsoft's Search backup page pairs with the suspend when the Search databases are backed up with SQL Server tools.
Then confirm every proxy landed in the default group, because a service application that works but whose proxy is not in the default group behaves exactly like one that is missing.
$pg = Get-SPServiceApplicationProxyGroup -Identity ''
$pg.Proxies | Select-Object DisplayName, TypeName | Format-Table -AutoSizePhase 4: web applications and customisations, before any content
Microsoft's service-application page ends by opening this phase:
"Now that the service applications are upgraded, you can start the process to upgrade the content databases. The first step in that process is to create the web applications that are needed for each content database."
So the web applications come first, matching the inventory.
# WEB APPLICATIONS. The URL must match the old farm exactly: it is written
# into links, into search results and into anything anyone ever bookmarked.
$appPoolAccount = Get-SPManagedAccount 'CONTOSO\svc-sp-content'
# Windows claims. Classic mode was REMOVED for content web applications in
# Subscription Edition. Microsoft's Subscription Edition pages do not say
# whether a farm still on classic converts on the old farm before the backup
# or here after the attach: rehearse the route on copies.
$ap = New-SPAuthenticationProvider
New-SPWebApplication -Name 'Contoso Intranet' `
-ApplicationPool 'ContosoIntranetPool' `
-ApplicationPoolAccount $appPoolAccount `
-AuthenticationProvider $ap `
-Port 443 -SecureSocketsLayer `
-HostHeader 'sharepoint.contoso.com' `
-Url 'https://sharepoint.contoso.com' `
-Certificate 'sharepoint.contoso.com' `
-UseServerNameIndication `
-DatabaseName 'SP_Content_Placeholder'
# Server Name Indication is what lets several web applications share port 443
# with their own certificates. Without it they all share one certificate.The placeholder database is deliberate: the web application needs one to exist, and the real content databases arrive in phase 5. Delete the placeholder afterwards, once the real databases are mounted.
One trap that costs an outage. If you later change bindings with Set-SPWebApplication, respecify all of them:
"All IIS binding settings should be respecified when updating the binding of an IIS website via the
Set-SPWebApplicationcmdlet. This includes the URL, secure sockets layer setting, the port number, the host header, and the certificate. If a binding setting isn't respecified, it will revert to its default value."
# Right: every binding setting stated, even the ones that are not changing.
Set-SPWebApplication -Identity 'https://sharepoint.contoso.com' -Zone Default `
-Port 443 -SecureSocketsLayer `
-HostHeader 'sharepoint.contoso.com' `
-Url 'https://sharepoint.contoso.com' `
-Certificate 'sharepoint.contoso.com' `
-UseServerNameIndicationThe customisations go in next, before any content database is tested. Testing before they are deployed is the ordering mistake that produces the longest and least useful troubleshooting session of the project. Test-SPContentDatabase reports every feature, template and assembly the content references and cannot find. Run it before the solutions are deployed and you get a report of hundreds of entries that are all the same problem.
Microsoft names it as one frequent cause:
"One frequent cause of failures during upgrade is that the environment is missing customized features, solutions, or other elements."
# SOLUTIONS. Driven from solutions.csv, produced by the inventory.
$wsps = Import-Csv 'C:\Migration\inventory\solutions.csv'
foreach ($w in $wsps) {
$path = Join-Path 'C:\Migration\wsp' $w.Name
if (-not (Test-Path $path)) {
throw "Missing required solution package: $($w.Name). Stop and resolve the dependency."
}
$solution = Add-SPSolution -LiteralPath $path
# GACDeployment is required when the package contains a global assembly.
# The inventory recorded that per solution, so it is not a guess.
$gac = [bool]::Parse($w.ContainsGlobalAssembly)
if ($w.ContainsWebApplicationResource -eq 'True') {
Install-SPSolution -Identity $solution -AllWebApplications `
-GACDeployment:$gac -Force
} else {
Install-SPSolution -Identity $solution -GACDeployment:$gac -Force
}
}
# Deployment is asynchronous: it runs on a timer job. Wait for it, or
# phase 5 tests against a farm that is still deploying.
$deploymentDeadline = (Get-Date).AddMinutes(30)
do {
if ((Get-Date) -gt $deploymentDeadline) { throw "Solution deployment exceeded the rehearsal budget." }
Start-Sleep -Seconds 15
$pending = Get-SPSolution | Where-Object { $_.JobExists }
Write-Host "Waiting for $($pending.Count) solution job(s)..."
} while ($pending)
Get-SPSolution | Select-Object Name, Deployed, DeploymentState | Format-Table -AutoSizeAnything on the missing list is a decision, not a warning. Either the package is found and rebuilt, or the sites depending on it are inventoried and dealt with before the migration, not during it.
Phase 5: test, then mount
Never mount first. Test-SPContentDatabase changes nothing, and it is the only thing that tells you what is missing while it is still cheap to fix.
Two parameters earn their keep. -ShowLocation tells you where a missing feature is used, which turns a GUID into a site somebody owns. It is slow, so it belongs in the rehearsal rather than the live run. And -ExtendedCheck:
"Checks for inconsistent authentication modes during database-attach upgrade process. The selected mode, claims or classic, must be the same in both versions."
That one matters more than it sounds, because classic mode was removed for content web applications in Subscription Edition. A farm still running classic authentication finds out here.
# TEST THEN MOUNT. Driven from the inventory, one database at a time.
$dbs = Import-Csv 'C:\Migration\inventory\contentdbs.csv'
$webApp = 'https://sharepoint.contoso.com'
$report = 'C:\Migration\reports'
New-Item -ItemType Directory -Path $report -Force | Out-Null
foreach ($db in $dbs) {
Write-Host "`n=== $($db.Name) ===" -ForegroundColor Cyan
# -ExtendedCheck catches a claims/classic mismatch. Classic mode is gone
# for content web apps, and Microsoft's Subscription Edition pages do not
# say whether to convert on the old farm before the backup or here after
# the attach: rehearse the route on copies.
$issues = Test-SPContentDatabase -Name $db.Name -WebApplication $webApp -ExtendedCheck
$issues | Export-Clixml "$report\$($db.Name).test.xml"
$issues | Format-List | Out-File "$report\$($db.Name).test.txt" -Encoding UTF8
# UpgradeBlocking is used here to separate "fix this now" from "write this
# down". No Subscription Edition page, nor the Test-SPContentDatabase
# reference, documents it: a search of Microsoft Learn finds it, with
# Category, Message and Remedy, only in sample output on the Project
# Server 2016 upgrade page. Confirm all four on the rehearsal farm.
# Nothing depends on them, because any reported issue stops the mount
# below. Read the file either way: a non-blocking missing web part is
# still a broken page for whoever put it there.
$blocking = $issues | Where-Object { $_.UpgradeBlocking }
if ($blocking) {
Write-Host "BLOCKED: $($blocking.Count) blocking issue(s). Not mounting." -ForegroundColor Red
$blocking | Format-List Category, Message, Remedy
throw "Blocking database diagnostics require resolution before continuing."
}
if ($issues) {
throw "Non-blocking diagnostics require documented review before mounting; see the saved report."
}
# Mount-SPContentDatabase, not Central Administration: attaching a database
# through the Central Administration pages is not supported for upgrading.
# Site collections upgrade automatically as part of this, which is the
# default and the recommended behaviour.
Mount-SPContentDatabase -Name $db.Name -WebApplication $webApp -Verbose
Write-Host "MOUNTED: $($db.Name)" -ForegroundColor Green
}If a database holds thousands of site collections and the mount window is tight, -SkipSiteUpgrade defers the site upgrade to first browse. It is a scheduling tool, not a shortcut: the work still happens, just spread out and under load from real users.
Mount-SPContentDatabase -Name 'WSS_Content_Archive' -WebApplication $webApp -SkipSiteUpgradeAnd the model changed. There are no compatibility modes any more:
"There is no concept of 'site collection compatibility modes' in SharePoint Server Subscription Edition. You must be running the latest version at all times."
That is Microsoft's site-collection page. Its upgrade overview still says "Site owners can then upgrade individual site collections", so its pages conflict on the model. This sequence follows the site-collection page, where site collections upgrade automatically once the databases are upgraded.
Phase 6: what to check before letting anyone in
# 1. Every site collection accounted for, against the inventory.
$before = Import-Csv 'C:\Migration\inventory\sites.csv'
$after = Get-SPSite -Limit All | Select-Object -ExpandProperty Url
$missing = $before.Url | Where-Object { $_ -notin $after }
if ($missing) { $missing | ForEach-Object { Write-Warning "MISSING SITE: $_" } }
else { Write-Host "All $($before.Count) site collections present." -ForegroundColor Green }
# 2. Site collection upgrade, per content database, through the cmdlet's
# documented filters for failed and in-progress upgrades. Site collections
# whose upgrade has not started, such as those in a database mounted with
# -SkipSiteUpgrade (phase 5), upgrade when first browsed. Only a SharePoint
# 2013 page says each filter finds just its own state; the reference does
# not, and has -HideWaiting for those site collections. Confirm in the
# rehearsal what these calls list, and never read an empty result as
# covering site collections whose upgrade has not started.
# The cmdlet's reference does not document the properties of what it
# returns, so the result is shown whole rather than read field by field.
# The error and warning counts are on the Upgrade Status page in Central
# Administration and in the Upgrade*.log files. The logs from Mount-based
# upgrades live inside those, not in separate SiteUpgrade files, which is
# where people go looking and find nothing.
Get-SPContentDatabase | ForEach-Object {
Get-SPSiteUpgradeSessionInfo -ContentDatabase $_ -ShowFailed
Get-SPSiteUpgradeSessionInfo -ContentDatabase $_ -ShowInProgress
} | Format-List
# 3. The index is EMPTY. The topology in the new farm is new, so a full crawl
# over the whole corpus is mandatory, and only once every content database
# is mounted. Starting it earlier just means crawling twice. Microsoft's
# documented way to start it is Start Full Crawl on each content source
# that does not have continuous crawls enabled, in Central Administration
# (Search Administration, Content Sources). Microsoft's crawl page gives no
# such command for a source with continuous crawls: see the note after this
# block. This lists the sources to start; each has finished when its Status
# reads Idle.
$ssa = Get-SPEnterpriseSearchServiceApplication
Get-SPEnterpriseSearchCrawlContentSource -SearchApplication $ssa
# 4. Which feature release ring this farm should be on. Standard is the
# default; Early gets new experiences sooner. Record the decision.
Get-SPFeatureReleasePreferenceMicrosoft's crawl page gives its per-source commands "for any content source that does not have continuous crawls enabled", and none for a source that has them. For such a source, switch continuous crawls off as its continuous crawls page describes, start the full crawl once the source's Status reads Idle, and switch them on again when it has finished: that is our procedure, not Microsoft's. The Start all crawls link on the Manage Content Sources page is not a substitute: the crawl page says it crawls the content sources incrementally unless the system detects that a full crawl is required, or the source is a SharePoint Sites source with continuous crawls enabled.
If the crawl has to start without anyone in Central Administration, the documented PowerShell route is a full-crawl schedule on each content source, set with Set-SPEnterpriseSearchCrawlContentSource -ScheduleType Full (reference); the same reference says that with continuous crawls enabled, "Search service application administrators can still configure full crawls as needed." Where People Search runs, Microsoft's crawling best practices put the profiles first, in their own sps3s:// content source, and then say to "Wait about two hours after the crawl for the People content source finishes" before the first full crawl of Local SharePoint sites.
Preserve the old farm under an agreed recovery policy. Routing back can recover access to its preserved state; it cannot preserve writes accepted by the target. Before reopening either side for writes, establish what changed and how it will be reconciled. A fixed one-week retention rule does not establish recoverability.
Rehearse it, then do it
Every number in the plan is a guess until the rehearsal produces it. Restore a copy of the production databases onto the new farm, run the whole sequence, and write down three things: how long each mount took, what Test-SPContentDatabase -ShowLocation found, and which of the missing features nobody could account for.
Then throw that farm away and do it again on the real window. The second run is the one with realistic timings, a solutions list that is already complete, and no surprises in the test report, because they were all found the first time.
Before you start
- Confirm the direction: 2016 and 2019 both go directly to Subscription Edition, and only 2013 and older need an intermediate hop
- Check that every database is at 16.0.4351.1000 or higher, because below that the upgrade is blocked outright
- Take the inventory before touching anything, and keep it in source control
- Do not use the Farm Configuration Wizard for the service applications whose databases you intend to upgrade
- Create the web applications after the service applications, as Microsoft documents, and reproduce the old URLs exactly
- Deploy the farm solutions before running
Test-SPContentDatabase, or the report is unreadable - Find the Secure Store passphrase from the old farm before you need it
- If 2013 workflows come across, move the App Management database with the Workflow Manager and Service Bus databases
- Run
Test-SPContentDatabasewith-ExtendedCheck, and read the report even when nothing is blocking - Set the databases back to read-write after restoring, because a read-only database cannot be mounted
- Respecify every binding when using
Set-SPWebApplication, or the ones you left out revert - Plan the full crawl for after the last content database is mounted: Start Full Crawl on each content source, profiles first where People Search runs, with continuous crawls switched off first where they run
- Preserve the source for the agreed recovery period, and define reconciliation for writes accepted after cutover
Documentation checkpoint, 2 November 2026
The examples are a documented starting point, not a farm-tested execution package. The deployment wait budget above is illustrative and must be measured in rehearsal. A timer job ending does not establish that every package deployed successfully: inspect each required solution and its deployment result before proceeding. Review package target scope rather than automatically approving deployment to every web application.
This revision checked the article against Microsoft's pages again on 2 November 2026. The phases now follow Microsoft's documented order: the web applications and the solutions moved from phases 1 and 2 to phase 4, after the service applications, because that is where Microsoft's pages put them. Phase 3 now names the App Management database that a farm moving its 2013 workflows brings across, and the KB5002799 prerequisite from the update notes. The article now says where it takes one side of a conflict between Microsoft's pages, on the User Profile Sync database and on the site-collection upgrade model, and the copy phase says where Microsoft's timing for the User Profile databases differs from this sequence. The reason the article gave for avoiding the Farm Configuration Wizard was not Microsoft's, so it has been removed. The scripts no longer read the Status, ErrorCount and WarningCount properties of Get-SPSiteUpgradeSessionInfo, which its reference does not document, or call StartFullCrawl(), for which a search of Microsoft Learn finds no documentation; they use the cmdlet's documented filters, and point to the documented Start Full Crawl in Central Administration instead. The Test-SPContentDatabase fields the test loop reads are now labelled as undocumented, and the article now says what happens to the old farm's suspended Search service application. The comments on classic authentication no longer say the conversion cannot happen later, because Microsoft's Subscription Edition pages do not say on which farm to convert. The phases diagram has been redrawn to the documented order.
The Server documentation contains a conflict: the content-database upgrade page still describes creating a classic-authentication web application, while the SE removed-features page explicitly removes classic authentication for content web applications. The -ExtendedCheck description quoted in phase 5 still offers "claims or classic" as well. Follow the version-specific removal boundary and resolve the authentication conversion in rehearsal; do not treat the older instruction as permission to recreate an unsupported mode.
- SE upgrade overview and database prerequisites
- Create the target farm and recreate configuration
- Upgrade the service applications
- Services upgrade overview
- Prepare customisations and upgrade content databases
- Upgrade a site collection
- Verify the upgraded environment
- Workflow Manager on a new farm, with the App Management database
- October 2026 update notes, KB5002927
- Manage a paused Search service application
- Back up Search service applications
- Run a farm that uses read-only databases
- Start, pause, resume or stop a crawl
- Manage continuous crawls
- Best practices for crawling
- Get-SPSiteUpgradeSessionInfo
- Test-SPContentDatabase
- Removed and deprecated SE features