Snakes in a Plane
Migration Guides

Search-Replace After Migration

Every WordPress migration between domains, or from a staging URL to a live one, needs a search-replace pass on the database — because WordPress stores full URLs directly in post content, in the wp_options table (siteurl, home), and inside serialized PHP arrays used by widgets, theme options, and many plugins. Get this step wrong and the front end usually still loads — but with broken images, dead internal links, or a redirect loop — which makes it a worse failure mode than an outright crash, because it looks “mostly fine” until a visitor clicks something.

Why a Plain Text Find-and-Replace Corrupts the Database

Serialized PHP data stores the byte-length of each string alongside the string itself — for example s:20:"http://old-site.test" tells PHP to read exactly 20 characters. If you run a naive SQL UPDATE ... REPLACE() or a plain-text editor find-and-replace against an exported .sql file, the URL text changes but the stored length number doesn’t update to match. PHP then reads the wrong number of characters, the unserialize call fails, and that specific option, widget, or theme-mod setting silently reverts to a default or blank value. This is the root cause of “my widgets disappeared after migration” and “my theme customizer settings reset” — not a lost setting, but a broken serialized string that WordPress can no longer parse.

The Right Tools: WP-CLI search-replace

wp search-replace is the standard for a reason: it deserializes each PHP array before touching the strings inside it, updates the byte-length counters correctly, then reserializes — so nothing breaks. The basic command is wp search-replace 'http://old-domain.test' 'https://new-domain.com' --all-tables. Always run it first with --dry-run to see a count of matches per table before committing, and always take a full database export immediately before running it for real, since it’s a destructive, non-undoable operation on the live tables: wp db export pre-searchreplace-$(date +%F).sql.

Common Mistakes

Forgetting the protocol. Searching for old-domain.test without the http:// or https:// prefix is usually the right call — it catches both protocol variants and protocol-relative URLs in one pass. But if the old site had both HTTP and HTTPS content saved at different times (common after a mid-life SSL migration), you may need two passes: one for each protocol variant, if the destination protocol also changed.

Running it before the DNS/URL is finalized. If you search-replace to the new domain before confirming that’s the final domain, you’ll need to run it again for whatever the URL actually ends up being — each run is safe and idempotent as long as you’re searching for the string currently in the database, but it’s wasted effort and an extra risk window.

Missing subdomain or www variants. old-site.test and www.old-site.test are different strings to a search-replace tool. If content references both forms (common when a site migrated its canonical www/non-www preference at some point), run a separate pass for each variant, or normalize to one form with the --regex flag.

Not checking --network on multisite. On a WordPress multisite install, URLs live in per-site tables plus network-wide tables (wp_blogs, wp_site). A search-replace run without --network and without targeting each site’s table prefix individually will miss network-level URL references, leaving the multisite dashboard pointing at the old domain even after every individual site looks correct.

Alternative Tools

Where WP-CLI shell access isn’t available (some restrictive shared hosts), the Better Search Replace plugin runs the same serialization-safe logic from the wp-admin UI, with a dry-run mode and per-table selection. The older but still-reliable Interconnect/IT Search Replace DB script runs directly against phpMyAdmin-exported files or a live connection when you don’t have wp-admin access at all yet (for example, mid-migration before DNS cuts over). Both handle serialization correctly — the failure mode described above only happens with raw SQL text replacement or a manual .sql file edit in a text editor.

Comparison Table

Tool Access needed Serialization-safe Best for
WP-CLI search-replace SSH Yes VPS/managed hosts with shell access
Better Search Replace (plugin) wp-admin Yes Shared hosting without SSH
Interconnect/IT Search Replace DB File upload or DB creds Yes Pre-launch, before wp-admin is reachable
Manual SQL / text editor replace Any DB access No — corrupts serialized data Never recommended

FAQ

Do I need to search-replace if I only changed hosts, not domains? No — if the domain and protocol stay identical, there’s nothing stored in the database that needs to change. Only a URL, protocol, or path change (e.g. staging subfolder to root domain) requires it.

Why did my customizer settings reset after I edited the .sql file directly? That’s the classic serialized-string corruption described above. Restore from your pre-migration backup and redo the URL change with wp search-replace instead of a text-editor find-and-replace.

Is it safe to run search-replace on a live production site? Yes, if you’ve taken a fresh backup first and run --dry-run to confirm the match count looks reasonable. It runs in a single transaction per table and completes in seconds on most WordPress databases.

Verdict

wp search-replace with --dry-run first and a backup immediately before the real run is the correct default for any URL change. Reach for Better Search Replace or Interconnect/IT’s script only when SSH access genuinely isn’t available — never fall back to a manual SQL or text-editor find-and-replace, since it’s the single most common cause of post-migration widget and customizer data loss.