Zero-Downtime WordPress Migrations: Lessons from 30+ Sites
The Migration That Looks Successful (But Isn't)
After 30+ WordPress/WooCommerce migrations, I've learned that the danger isn't the migration that fails loudly — it's the one that appears to succeed.
Here are the silent failures that have caused real production incidents, and the processes built to prevent them.
WP Importer's Silent Failures
WP Importer is not a migration tool. It's a content-import tool, and it has fundamental limitations that aren't documented prominently:
- Menu hierarchy is lost. Nested menu items become flat. The
_menu_item_menu_item_parentpost meta doesn't survive the round-trip reliably. _menu_item_classesis stripped. Custom CSS classes on menu items disappear silently.- The
--allow-updatesflag doesn't exist in WP-CLI's importer. It's a myth that persists in blog posts.
The fix: use rsync for file transfer and MySQL dump/import for the database. WP Importer is only appropriate for moving content between incompatible environments (different hosting, different DB credentials).
Attachment ID Collisions
When you clone a production database to staging, WooCommerce order IDs occupy the same auto-increment ranges as attachment IDs. If staging accumulates test orders, a subsequent production clone will have attachment IDs that collide with staging's order IDs.
The result: images that point to order objects instead of media files. ACF image fields that return incorrect post IDs.
The fix: always reset the staging database completely before a production clone. Never accumulate staging state across migration cycles.
Blocksy Content Blocks Are NOT Theme Mods
This one cost me a day. Blocksy's "Content Blocks" (headers, footers, hooks) look like theme customizer settings, but they're stored as a custom post type (ct_content_block) — not in wp_options as theme mods.
A theme export/import (via wp customize export) won't capture them. They require a separate post-type migration with ID remapping if any other content references those IDs.
Fluid Checkout's 69 Hidden Options
Fluid Checkout stores its configuration in 69+ fc_* options in wp_options — not in theme mods, not in a single serialized option. A database clone captures all of these correctly.
But if you migrate content only (via WP Importer or WP-CLI post export), the checkout configuration is missing entirely. The checkout page renders, but every Fluid Checkout feature is at its default state.
Always migrate the full database for WooCommerce sites.
The Go-Live Simulation
Before any production DNS cutover, we run a "go-live simulation" — a full dry run on a GLS staging environment that mirrors production DNS via /etc/hosts overrides.
The simulation runs 27 E2E scenarios across 6 Playwright spec files: homepage, navigation, product listing, product detail, cart, checkout, account pages, and WooCommerce email flows.
If any scenario fails, the go-live is blocked. No exceptions.
This caught a Freemius license activation bug (caused by a stdClass vs array mismatch in fs_accounts) that would have caused a fatal error on the live site within 48 hours of launch.
The S2S Transfer Protocol
Kinsta-to-Kinsta transfers via relay (local machine as intermediary) are 3-5x slower than direct server-to-server transfers. The solution: generate a temporary ed25519 key pair on the destination server, add the public key to the source server's authorized_keys, run rsync directly between servers, then remove the temporary key.
For a 2GB site, this reduces transfer time from 20 minutes to 4 minutes.
