Two technical challenges frequently cause anxiety for WordPress site administrators and developers: executing a flawless server migration without downtime, and architecting a WordPress Multisite network that scales cleanly without collapsing under maintenance debt.
When handled carelessly, both procedures carry severe business risks. A botched database search-replace breaks serialized arrays, leaving themes and custom fields broken. Similarly, choosing Multisite for an incompatible project structure creates an architectural bottleneck where a single plugin conflict or PHP fatal error takes down dozens of web properties simultaneously.
In this technical guide, we break down the engineering principles behind reliable zero-downtime migrations using WP-CLI and explore the multi-tenant architecture of WordPress Multisite. Migration and Multisite are two separate decisions that often get mixed up: a migration moves files, database and DNS, while Multisite changes how several sites share one WordPress codebase. The official reference for the network side is the WordPress Multisite handbook.
For more insights into our production infrastructure and development practices, explore our WordPress development services.
Preserving database integrity during WordPress migrations
The primary reason WordPress migrations fail is not missing files; it is corrupted database records. WordPress stores a substantial portion of theme options, widget configurations, and plugin data (such as Advanced Custom Fields or page builder layouts) as serialized PHP strings.
Why a SQL replace breaks serialized PHP data
Consider how WordPress stores configuration data inside the wp_options table:
a:2:{s:4:"host";s:14:"old-domain.com";s:4:"port";i:3306;}In this string, s:14 explicitly declares that the string old-domain.com is exactly 14 characters long. If you open a raw .sql file in a generic text editor or run a crude UPDATE wp_options SET option_value = REPLACE(...) query to swap the domain to example-shop.com (16 characters), the length declaration remains s:14.
When WordPress next queries the database and calls unserialize(), the PHP parser encounters 16 characters instead of the promised 14. The parser throws an unserialize(): Error at offset notice, fails closed, and discards the entire array. As a result, widgets vanish, layout options revert to defaults, and API keys disappear.
How WP-CLI search-replace fixes serialized data
The official WordPress command-line interface (WP-CLI) natively handles serialization. It pulls objects into memory, deserializes them into PHP data structures, performs the string replacement, recalculates string lengths, and re-serializes the data before committing changes to MySQL. The search-replace reference also states that it never changes primary key values. Keep the dry run identical to the real run, so the preview shows exactly what will change:
# 1. Dry run: same tables and flags as the real run, nothing is written
wp search-replace 'https://old-domain.com' 'https://new-domain.com' \
--all-tables --skip-columns=guid --dry-run
# 2. Execute the replacement
wp search-replace 'https://old-domain.com' 'https://new-domain.com' \
--all-tables --skip-columns=guid
# 3. Flush object cache and regenerate rewrite rules
wp cache flush
wp rewrite flush --hardTwo flags often copied from old tutorials need a correction. --recurse-objects is already the default, so adding it changes nothing. --precise is not what makes serialized data safe: by default the command runs fast SQL queries and switches to PHP on its own for columns that contain serialized data, while --precise forces PHP on every column, which is slower and only worth it for unusually complex structures. --skip-columns=guid matters more, for the reason explained in the site URL section below.
How to migrate WordPress with zero downtime
To move high-traffic e-commerce stores or lead generation portals without dropping transactions or confusing search engine crawlers, adhere to this four-phase operational sequence:
Phase 1: DNS TTL reduction (48 hours prior)
Lower the Time to Live (TTL) on your domain’s DNS A records to 300 seconds (5 minutes). This ensures that global DNS caches refresh rapidly when the new server IP is published.
Phase 2: Incremental file sync with rsync
Traditional FTP is slow, lacks encryption, and fails silently on large media libraries. Use rsync over SSH to mirror files directly between servers, excluding transient cache directories:
rsync -avzP --delete \
--exclude 'wp-content/cache/*' \
--exclude 'wp-content/uploads/cache/*' \
--exclude '*.log' \
[email protected]:/var/www/vhosts/site.com/public/ /var/www/html/The -a flag preserves file permissions and timestamps, while -z compresses data over the wire.
Phase 3: Cutover window
- Enable maintenance mode on the source server:
wp maintenance-mode activate. - Generate a clean database dump:
wp db export cutover.sql --add-drop-table. - Transfer and import the dump onto the new host:
wp db import cutover.sql. - Run
wp search-replaceto update site URLs, SSL protocols, or path differences. - Verify site health locally by mapping the domain to the new IP in your local
hostsfile.
Phase 4: DNS switch and verification
Update the DNS A record to point to the new server IP. Because the TTL was reduced to 300 seconds in Phase 1, traffic migrates smoothly to the new host within minutes. Once DNS has fully propagated globally, restore standard TTL (86400 seconds).
How WordPress multisite works for multiple sites
WordPress Multisite enables organizations to manage hundreds or thousands of virtual websites from a single codebase and database instance.
WordPress multisite database tables
A standard WordPress install contains 12 core tables. In Multisite, WordPress separates global shared data from tenant-specific data:
- Global Network Tables:
wp_usersandwp_usermeta: User accounts exist globally once across the network. A user can be an Author on Site 1 and an Administrator on Site 2.wp_blogs,wp_site,wp_sitemeta: Track network-level settings, registration policies, and active sites.
- Tenant-Specific Tables:
- Each child site receives an isolated set of content tables identified by site ID: Site 2 operates on
wp_2_posts,wp_2_options, andwp_2_comments.
- Each child site receives an isolated set of content tables identified by site ID: Site 2 operates on
+-----------------------------------------------------+
| WordPress Multisite Schema |
+-----------------------------------------------------+
| Network Global Tables: |
| - wp_users / wp_usermeta |
| - wp_blogs / wp_site / wp_sitemeta |
+-----------------------------------------------------+
| Primary Site (ID 1): |
| - wp_posts, wp_options, wp_terms... |
+-----------------------------------------------------+
| Subsite (ID 2 - e.g. london.brand.com): |
| - wp_2_posts, wp_2_options, wp_2_terms... |
+-----------------------------------------------------+
| Subsite (ID 3 - e.g. paris.brand.com): |
| - wp_3_posts, wp_3_options, wp_3_terms... |
+-----------------------------------------------------+When to use WordPress multisite
- Franchise and Regional Networks: When 100 regional dealerships share the same theme, approved plugins, and core branding, differing only in inventory and localized contact details.
- Higher Education and Institutional Portals: Giving academic departments and student organizations standardized blogs under an overarching institutional domain.
- Internal Staging and Product Demos: Instantly provisioning sandboxed sites with pre-installed configurations via automated scripts.
Multilingual content alone is rarely a reason for Multisite. WPML, Polylang or language variants inside one installation are often simpler to run than a site-per-language network. The handbook makes a related point: if the sites must share data or users heavily, or only need different themes per section, a single site with a plugin may fit better than a network.
When to avoid WordPress multisite
- Diverse Agency Client Rosters: One poorly coded plugin or memory leak on Client A’s subsite can crash the entire PHP process pool, bringing down Clients B, C, and D.
- Divergent Plugin Requirements: If site A requires WooCommerce with complex ERP connectors and site B requires an advanced forum, maintaining a shared plugin directory becomes fragile and prone to dependency conflicts.
- Separate hosting needs: When sites need different PHP versions, different hosting plans or different security zones, one network cannot give them that.
- Future Extraction Complexity: Decoupling a single subsite from a Multisite database into an independent standalone site requires exporting filtered SQL tables, rewriting table prefixes, and re-associating users.
Requirements before creating a WordPress multisite network
The Before you create a network chapter lists restrictions that the Network Setup screen enforces, and most failed conversions run into one of them.
Network creation is blocked when the WordPress address uses a port other than 80 or 443. A sub-domain install is blocked when the WordPress URL contains a path (WordPress is not at the document root), when the address is localhost, or when it is an IP address such as 127.0.0.1. For local testing the handbook suggests mapping a custom hostname to 127.0.0.1 in the hosts file instead. A sub-directory install is blocked when the existing installation is more than a month old, because the main site’s posts would clash with subsite paths; on a path-based network the main site’s post URLs also get a /blog/ prefix.
Server side, Apache needs mod_rewrite with .htaccess support, FollowSymLinks not disabled, and AllowOverride set to All or Options All for the vhost. Pretty permalinks must already work on the single site. Settle the domain, including the www question, before activation, and if WordPress should run from its own directory, make that change first too. Wildcard DNS is only required when users create sub-domain sites on demand; sites added by hand can use explicit DNS records or virtual hosts.
How to create a WordPress multisite network step by step
The Create a network procedure has an order that matters. Back up the database and files. Deactivate all plugins; they can be reactivated once the network exists. Then add the switch to wp-config.php above the line /* That's all, stop editing! Happy publishing. */:
// Step 1: Enable the network setup menu in the WordPress dashboard
define('WP_ALLOW_MULTISITE', true);Navigate to Tools > Network Setup in the dashboard, choose between Sub-domains (subsite.domain.com) or Sub-directories (domain.com/subsite/), and paste the generated constants into wp-config.php directly after the WP_ALLOW_MULTISITE line:
// Step 2: Activate the production network configuration
define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', true);
define('DOMAIN_CURRENT_SITE', 'domain.com');
define('PATH_CURRENT_SITE', '/');
define('SITE_ID_CURRENT_SITE', 1);
define('BLOG_ID_CURRENT_SITE', 1);The same screen prints rewrite rules. They replace the existing WordPress block in .htaccess (on Nginx they become server block rules). Back up both files before editing them. After saving, log in again through the link WordPress shows; clearing browser cookies is sometimes needed. “My Sites” then gains a Network Admin menu. Switching between sub-domains and sub-directories later is possible, but the handbook warns that it might not be easy, so choose before the first subsite exists.
Domain mapping in multisite without a plugin
Since WordPress 4.5, native domain mapping is baked directly into the core engine. You can map custom top-level domains (e.g. clientdomain.com) directly within Sites > Edit Site in the Network Admin panel without installing third-party mapping plugins. On the web server layer, configure your Nginx or Apache server blocks to accept requests for mapped domains and forward them to the primary WordPress root.
The domain mapping chapter adds the order of work. Point every domain at the server in DNS first. Install TLS for the primary domain and use SNI certificates for the others, so every admin login stays encrypted. Then enter the full URL, for example https://clientdomain.com, in the Site Address (URL) field of that subsite. If logging in on a mapped site fails with a blocked-cookies error or silently, add this line after the network constants:
define( 'COOKIE_DOMAIN', $_SERVER['HTTP_HOST'] );When moving a site to its own domain, lower the TTL a day ahead, make sure the server answers on the new hostname, and only then change the Site Address. Run a serialization-safe search-replace for old URLs in posts, media and options afterwards.
User roles across sites in WordPress multisite
In Multisite, users live in one shared table. Membership and roles are assigned per site.
| Role | Typical responsibility | Scope |
|---|---|---|
| Super Admin | Plugins, themes, new sites, network settings | Whole network |
| Administrator | Content and users on their site | One site |
| Editor / Author | Editorial content | One site |
| Subscriber | Login or membership | Per site |
What that means in practice:
- An Administrator on one subsite cannot install plugins. That is a Super Admin job.
- Deleting a user from Network Admin removes the account everywhere, not only on one branch site.
- Single sign-on across several brands comes naturally with Multisite; isolation between clients does not.
Document who holds Super Admin and keep the number small. Too many Super Admins is the usual reason behind “someone activated a plugin in Network Admin and three sites went down”.
How to merge WordPress sites into multisite
Enabling a network on an existing installation, as described above, is not the same as merging five single installations. Every additional site has to come in through export and import (WXR), WP-CLI, or a tool that understands Multisite table prefixes.
Recommended order for a merge:
- Take a full file and database backup of every source site (3-2-1: three copies, two media, one off-site).
- Build the network on staging with the same PHP version as production.
- Create empty sites in Network Admin with their final domains or sub-domains.
- Import content site by site. Then run search-replace for old URLs inside that site’s table set only, using
--url=<site>without--network. - Map users: shared email addresses become one account; roles must be assigned per site.
- Verify media. Uploads for a subsite live under
wp-content/uploads/sites/{id}/. - Lower DNS TTL, switch, and keep rollback SQL ready.
How to move one site out of WordPress multisite
Sometimes the network is the wrong model. In that case you export one site out:
- Identify the
blog_idand table prefix (wp_5_and so on), for example withwp site list --fields=blog_id,url. - Export the content (WXR) or clone the tables into a new database with the standard
wp_prefix. - Copy media from
uploads/sites/{id}/to the regularuploads/folder. - Update
siteurlandhome, then run search-replace. - Install only the plugins and themes the site actually used, not the whole network directory blindly.
Plan redirects from the old sub-domain URLs to the new top-level domain. Otherwise you lose link equity and get 404 noise in Search Console.
How to change the WordPress site URL after a move
Settings > General holds two fields, WordPress Address (URL) for where the core files live and Site Address (URL) for the address visitors type. When a move leaves the admin unreachable, the migration chapter documents four ways to fix them. These are for single installs; on Multisite the handbook sends you to the database, covered in the next section.
- wp-config.php:
define( 'WP_HOME', 'https://example.com' );anddefine( 'WP_SITEURL', 'https://example.com' );. This hard-codes the values, and the General settings fields can no longer be edited. - functions.php: two
update_option()calls forsiteurlandhomein the active theme, loaded once, then removed. The handbook is explicit that the code must not stay in the file. - RELOCATE:
define('RELOCATE', true);inwp-config.php, then openwp-login.phpon the new address. It only fixes the Site URL, nothing else, and leaving it in place is insecure because in some configurations it lets an attacker change the site URL. Remove it right after. - Database or WP-CLI: edit
siteurlandhomeinwp_options, or runwp option update home 'https://example.com'andwp option update siteurl 'https://example.com'.
The same chapter has a rule worth repeating: never change the guid column in wp_posts. Feed readers use the GUID to recognise posts they have already shown, so rewriting it makes old posts reappear as new. That is why the WP-CLI example in the docs uses --skip-columns=guid. Cached copies of the old address can also sit in transients; they are safe to delete in the migrated copy (wp transient delete --all) because WordPress rebuilds them on demand.
Moving WordPress to another directory on the same server has its own order: change both URLs in Settings first, then move the files. If the files moved first, the handbook’s workaround is a temporary symlink from the old path to the new one (ln -s /path/to/new /path/to/old), after which the normal steps work.
How to run wp search-replace on a multisite network
On Multisite, wp search-replace only touches the current site’s tables unless you say otherwise. The command reference defines the scopes:
- no flag: tables registered to
$wpdbfor the site given by--url(the main site by default), --network: all tables registered to$wpdbacross the network,--all-tables-with-prefix: any table with the WordPress prefix, registered or not,--all-tables: every table in the database, whatever its prefix. It overrides--network, so on a database shared with other applications it reaches their tables too.
Tables without a primary key are skipped silently; wp db query 'DESCRIBE <table>' shows whether PRI is present. The documented way to turn a production network into a local copy also rewrites the network tables explicitly:
wp search-replace --url=example.com example.com example.test \
'wp_*options' wp_blogs wp_site --network --skip-columns=guid--export=network.sql writes the transformed data to a file instead of the live database, which suits a cutover where the import happens on the new host. Before and after the change, list every site with wp site list and compare the stored addresses:
for url in $(wp site list --field=url); do
echo "$url $(wp option get siteurl --url="$url")"
doneThe handbook’s Multisite move notes set the scope. Same domain on a new server: copy files and database exactly as for a single install. New domain: move the files, update DOMAIN_CURRENT_SITE in wp-config.php and .htaccess if the folder changed, then review wp_site and wp_blogs (domain and path) and, in every wp_N_options table, home, siteurl and fileupload_url. Switching between sub-domains and sub-directories also means changing SUBDOMAIN_INSTALL and the rewrite rules.
Common WordPress multisite staging mistakes
Staging is where Multisite most often goes wrong, not production.
URLs hard-coded in the network. A cloning tool that only swaps the main domain but leaves wp_blogs and wp_site pointing at production logs editors into the wrong environment or mixes cookies.
Search-replace without excluding the GUID or without a dry run. Always run the dry run. Limit the tables when you only move one site.
Cron and email. Network cron (wp-cron.php) and outgoing mail from staging must not reach real customers. Block outgoing SMTP on staging or route it to a mail sink.
Staging sub-domains. staging.brand.com needs its own DNS and usually its own TLS certificate. A wildcard certificate for *.brand.com does not cover staging.otherbrand.com. Test HTTPS before inviting content editors.
Network-level plugin activation. A test plugin on staging is fine. The same action in production without a canary site is not. Keep at least one canary site where new plugins are activated first.
WooCommerce in a network. If the network is a chain of shops, test payment gateway callbacks per site URL. Webhooks that still point at staging after go-live are expensive. Without e-commerce, keep the migration to content, media and roles.
Multisite, plain migration or a separate install
| Situation | Recommendation | Why |
|---|---|---|
| 8 franchise locations, same theme | Multisite with sub-domains or mapping | Shared plugins, shared login |
| 5 clients in different industries | Separate installations | Fault isolation and legal separation |
| One company, three languages | Often one install plus a language plugin | Simpler SEO and menus than three sites |
| University with student blogs | Multisite | Scalable site creation |
| Changing hosting, keeping the model | Plain migration (files, DB, DNS) | Multisite is irrelevant to the move itself |
| One brand leaving the network | Export one site to a single install | Smaller blast radius |
What belongs on a WordPress migration checklist
- Backup verified with a restore test, not just a file size.
- Same PHP version and extensions on staging and production.
wp search-replace --dry-runwithout surprises, with the same flags as the real run.- Login tested as Super Admin and as a site Administrator.
- Media URLs load over HTTPS on every mapped domain.
wp site list --field=urlmatches the expected list of addresses.- DNS TTL lowered; rollback plan written down (who flips the A record, where the previous SQL dump lives).
Migration is mechanics. Multisite is an operating model. Mix them up and you end up fixing DNS when you should have separated the clients, or the other way round.







