WordPress database URL migration with WP-CLI search-replace

WordPress database URL migration with WP-CLI search-replace

Last verified: October 14, 2026
23 min read
Tutorial
Server architecture

Moving a WordPress site from a development environment to the production domain, renaming a brand or rolling out an SSL certificate (moving from HTTP to HTTPS) are routine jobs for every administrator and developer. At first glance the operation looks trivial: export the database to a .sql file, open it in a code editor, run “Find and replace”, then import the file back. Or run a quick SQL query in phpMyAdmin.

Doing that to a production WordPress database breaks the site straight away. Widgets vanish from the footer and sidebars, the theme’s colour and typography settings reset, advanced fields (ACF) return empty values, and e-commerce plugin settings are wiped.

The cause is the way WordPress core and plugins store complex data structures in a relational MySQL or MariaDB database. The WordPress documentation warns about it directly: a plain search and replace across the whole database can break serialization, because some themes and widgets store a URL together with its length (Migrating WordPress, developer.wordpress.org). Understanding PHP serialization and knowing the right tools, above all the WP-CLI command line, is the foundation of managing WordPress sites safely. For the wider process around the database, see the WordPress database migration guide.


#What is serialized data in the WordPress database

Relational SQL databases are designed to store scalar data: numbers, short strings and dates. WordPress, however, often needs to save a complex, multi-level structure in a single table row, for example an array holding the list of active widgets, their order, assigned icons and configuration parameters.

Instead of creating dozens of extra normalised tables for every plugin, WordPress developers chose a native PHP function: serialize().

// Example theme configuration array in PHP
$theme_settings = array(
    'site_name'    => 'My WordPress Shop',
    'logo_url'     => 'https://old-domain.com/logo.png',
    'contact_mail' => '[email protected]'
);

// Result of serialize($theme_settings)
// Stored directly in the option_value column of wp_options:
a:3:{s:9:"site_name";s:17:"My WordPress Shop";s:8:"logo_url";s:31:"https://old-domain.com/logo.png";s:12:"contact_mail";s:21:"[email protected]";}

#How to read a serialized string

Take a closer look at the fragment that holds the logo URL:

s:31:"https://old-domain.com/logo.png";

Every element in PHP’s serialized format has a strict structure:

  • s is the data type: string.
  • 31 is the declared length of the string (the exact number of bytes).
  • "https://old-domain.com/logo.png" is the actual value in quotes.

The number 31 is the key to the whole puzzle. When PHP reads the data from the database it calls unserialize(). The PHP parser does not search blindly for the closing quote: it counts exactly 31 bytes from the given position.

#Why SQL REPLACE breaks serialized data

Imagine moving the site to a new, longer domain: https://new-and-much-longer-company-domain.com. The new logo URL, https://new-and-much-longer-company-domain.com/logo.png, is 55 characters long.

If you run a classic SQL query:

-- CRITICAL ERROR: this query destroys serialization!
UPDATE wp_options
SET option_value = REPLACE(option_value, 'https://old-domain.com', 'https://new-and-much-longer-company-domain.com');

the database ends up with this string:

s:31:"https://new-and-much-longer-company-domain.com/logo.png";

What happens when a visitor loads the site:

  1. WordPress fetches the row from the database and passes it to unserialize().
  2. The function reads the s:31 header and expects exactly 31 bytes.
  3. PHP cuts off the first 31 characters of the new address and finds random letters inside the string where the closing quote and semicolon should be.
  4. The PHP parser raises PHP Notice: unserialize(): Error at offset... and returns false.
  5. The theme or plugin receives false instead of the expected options array.
  6. WordPress treats the data as missing and restores the theme’s factory defaults or drops the widgets.

#Where WordPress stores URLs in the database

URLs in a WordPress install do not live in one place. They are spread across many core tables and tables created by plugins:

graph TD
    A["WordPress database"] --> B["wp_options"]
    A --> C["wp_posts"]
    A --> D["wp_postmeta"]
    A --> E["wp_comments / wp_commentmeta"]
    A --> F["wp_usermeta"]
    A --> G["Plugin tables (WooCommerce, forms, SEO)"]

    B --> B1["siteurl and home (main addresses)"]
    B --> B2["sidebars_widgets (serialized)"]
    B --> B3["theme_mods (serialized)"]

    C --> C1["post_content (links, images)"]
    C --> C2["post_excerpt"]
    C --> C3["guid (do NOT change on published posts)"]

    D --> D1["_elementor_data (escaped JSON)"]
    D --> D2["_wp_attached_file (media paths)"]
    D --> D3["ACF fields (serialized)"]

#1. The wp_options table

This is the most important configuration table. It holds two critical records that decide whether the site works at all:

  • siteurl: where the WordPress core files physically live.
  • home: the main address of the site that visitors type into the browser.

Besides those, the table stores in serialized form:

  • sidebars_widgets: the widget layout in sidebars and footers.
  • theme_mods_[theme_name]: colours, header layout, typography and logo from the Customizer.
  • Settings of SEO plugins (Rank Math, Yoast), security plugins and cache plugins.

#2. The wp_posts table

Stores the content of posts, pages, WooCommerce products and media library items.

  • The post_content column: absolute links to other pages, embedded <img> tags, links to PDF files and Gutenberg block comments.
  • The guid column (Globally Unique Identifier): the unique identifier of a post. Important rule: the WordPress documentation says: “Never, ever, change the contents of the GUID column, under any circumstances.” (Migrating WordPress, developer.wordpress.org). Feed readers use the guid column to detect new articles. Changing it makes feed readers treat every old post as new and send repeat notifications to subscribers.

#3. The wp_postmeta table

Holds extra attributes of posts and pages. This is where the serialized arrays of Advanced Custom Fields (ACF), attachment metadata (_wp_attachment_metadata) and page builder configuration from Elementor or Divi end up.

#4. The wp_usermeta and wp_comments tables

They hold the website URLs of comment authors and user profile metadata.


#How to change the WordPress domain with WP-CLI search-replace

The WordPress command line interface (WP-CLI) is the fastest, most reliable and fully scriptable tool for migrating URLs. It runs directly on the server over SSH and works on PHP data structures in memory.

If WP-CLI is not installed yet, the official installation steps in the WP-CLI handbook download the phar file, make it executable and move it into the path:

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info

#Step 1: back up the database

Before you touch a production database, dump it to an SQL file:

# Database dump with DROP TABLE statements
wp db export backup-before-migration-$(date +%Y%m%d_%H%M%S).sql --add-drop-table

If the database gets damaged, you can restore the original state in seconds:

wp db import backup-before-migration-*.sql

#Step 2: run a dry run (—dry-run)

The --dry-run flag shows how many rows in which tables would change, without changing anything in the database:

wp search-replace 'https://old-domain.com' 'https://new-domain.com' --all-tables --dry-run

The output is a table with the columns Table, Column, Replacements and Type, plus a summary line with the total number of replacements. The Type column shows whether a column was processed in SQL or in PHP. If the count is zero, the old address usually differs from the one stored in the database by protocol, the www prefix or a trailing slash.

#Step 3: run the real replacement

For the final replacement we recommend this set of flags:

wp search-replace 'https://old-domain.com' 'https://new-domain.com' \
  --all-tables \
  --precise \
  --recurse-objects \
  --skip-columns=guid \
  --report-changed-only

What each parameter does:

  • --all-tables: searches every table in the database, including custom tables from e-commerce, form and logging plugins.
  • --precise: forces PHP instead of SQL for all columns. The WP-CLI documentation describes this as slower but more reliable with complex serialized data.
  • --recurse-objects: recurses into serialized objects. It is on by default and turned off with --no-recurse-objects, so writing it out only documents the intent.
  • --skip-columns=guid: skips the guid column in wp_posts, which protects feed readers from duplicate entries.
  • --report-changed-only: lists only the tables where something actually changed and hides hundreds of empty rows.

To restrict the run to a few tables, pass their names as arguments, for example wp search-replace 'https://old-domain.com' 'https://new-domain.com' wp_posts wp_postmeta. For a case-insensitive match, combine --regex with --regex-flags='i'; the documentation notes that regex mode is much slower.

#How to change the domain in a WordPress multisite network

In multisite installs (a network of sites) the core tables carry per-site prefixes (for example wp_2_posts, wp_3_options), and the domains of the individual subsites live in the wp_blogs and wp_site tables.

Here you use --network, which covers every table registered in $wpdb for the whole network, or --all-tables-with-prefix, which adds prefixed tables that are not registered in $wpdb:

wp search-replace 'https://old-network.com' 'https://new-network.com' --network --all-tables-with-prefix

The WordPress documentation admits that for a multisite domain change “This step cannot yet be easily automated.” (Migrating WordPress, developer.wordpress.org). After the replacement, check by hand:

  • the wp_site and wp_blogs tables, which store the domain and path of the network and of every subsite,
  • the home, siteurl and fileupload_url options in each site’s options table (wp_options, wp_2_options and so on),
  • when switching between subdomains and subdirectories: the .htaccess file and the SUBDOMAIN_INSTALL constant in wp-config.php.

#How to export the database with new URLs to an SQL file

If you prepare a database for a client on a local environment and want a ready SQL file with the domains replaced, without touching your local working database:

wp search-replace 'https://localhost/project' 'https://client-domain.com' \
  --all-tables \
  --export=database-ready-to-import.sql

#How to move from www to the bare domain

When only the prefix changes, replace the full address including the protocol, so you do not hit other strings that contain the same name:

wp search-replace 'https://www.yourdomain.com' 'https://yourdomain.com' --all-tables --precise --skip-columns=guid --dry-run

#How to change URLs with the Better Search Replace plugin

If you manage a site on shared hosting without SSH, or prefer to work from the WordPress dashboard, the safest choice is the Better Search Replace plugin, maintained by WP Engine (formerly Delicious Brains).

Step by step in the WordPress dashboard:
1. Go to Plugins > Add Plugin and search for "Better Search Replace".
2. Install and activate the plugin.
3. Go to Tools > Better Search Replace.
4. In the "Search for" field enter the old URL (e.g. https://old-domain.com).
5. In the "Replace with" field enter the new URL (e.g. https://new-domain.com).
6. Select every table in the list (Ctrl+A on Windows or Cmd+A on macOS).
7. Make sure "Run as dry run?" is CHECKED.
8. Click "Run Search/Replace" and review the report.
9. Uncheck "Run as dry run?" and run it again to save the changes.

#Better Search Replace errors and how to fix them

  • 504 Gateway Timeout or exceeded script execution time (max_execution_time): On large databases the web server can cut the HTTP request short. In the plugin’s Settings tab, lower Max Page Size. The default is 20000 rows per chunk, as the plugin source code shows (class-bsr-admin.php); a smaller chunk means more, shorter requests.

#How to change siteurl and home in WordPress

Two options in the wp_options table decide whether you can even log in at the new address. The WordPress documentation (Changing The Site URL) separates them like this: home is the “Site Address (URL)”, the address visitors type into the browser, and siteurl is the “WordPress Address (URL)”, the place where the core files live. Both fields in Settings > General should include https:// and should not end with a slash.

The documentation describes several ways to change these values. Each fits a different situation:

MethodWhen to use itWhat to watch
wp option update in WP-CLIYou have SSH, the dashboard is downChanges only the two options, wp search-replace fixes the rest
WP_HOME and WP_SITEURL constants in wp-config.phpYou need the site up quicklyValues are hard-coded, the fields in Settings > General can no longer be edited
update_option() in the theme’s functions.phpYou only have FTPRemove the code right after the dashboard loads
The RELOCATE constantThe site moved to another directory or serverFixes only the admin URLs, and is insecure if left in the file
Editing wp_options in phpMyAdminYou only have the hosting panelBack up first; the table prefix may not be wp_

#How to change siteurl and home with WP-CLI

The documentation gives two commands, followed by a content replacement that skips the guid column:

wp option update home 'https://new-domain.com'
wp option update siteurl 'https://new-domain.com'
wp search-replace 'https://old-domain.com' 'https://new-domain.com' --skip-columns=guid

#How to change the site URL in functions.php

When you only have FTP access, the documentation suggests adding two lines to the active theme’s functions.php, right after the opening <?php:

update_option( 'siteurl', 'https://new-domain.com' );
update_option( 'home', 'https://new-domain.com' );

Upload the file, load the login screen or the dashboard a couple of times, then remove the lines. The documentation is explicit: “Do not leave this code in the functions.php file.” (Migrating WordPress, developer.wordpress.org). The code would run on every request and overwrite options you later want to change from the dashboard.

#How to use the RELOCATE constant after moving a directory

The constant define( 'RELOCATE', true );, added to wp-config.php above the That's all, stop editing! comment, makes WordPress accept the current address after you log in through wp-login.php on the new server. According to the documentation it fixes only the admin URLs, not the rest of the configuration, and leaving it in place is insecure: “Always remove the RELOCATE line from wp-config.php after you’re done.” (Migrating WordPress, developer.wordpress.org).

#Transients in a migrated database

The documentation points out transients, the temporary cached values stored in wp_options. They can hold old URLs, and since WordPress rebuilds them on its own, they are safe to delete from the migrated copy of the database. WP-CLI does it with wp transient delete --all, and the documentation also gives a query for phpMyAdmin, to be run after a backup:

DELETE FROM `wp_options` WHERE `option_name` LIKE '%\_transient\_%';

#How to change URLs when the WordPress dashboard is down

In an emergency, when there is no SSH and the WordPress dashboard is down because of a redirect loop or damaged files, the long-standing tool is the Search-Replace-DB script (known as SRDB) made by the agency Interconnect/it.

How to use the SRDB script:
1. Download the official zip archive from interconnectit.com.
2. Unpack it and rename the folder to a unique, hard-to-guess string (e.g. srdb-sec-8f7a9d/).
3. Upload the folder over FTP / SFTP to the WordPress root (where wp-config.php is).
4. Open https://your-domain.com/srdb-sec-8f7a9d/ in the browser.
5. The script reads the database credentials from wp-config.php automatically.
6. Fill in the "replace" (old value) and "with" (new value) fields.
7. Click "dry run", and after checking, "live run".
8. CRITICAL: when finished, click the red "delete me" button so the script removes itself from the server.

[!CAUTION] Leaving the Search-Replace-DB script on a live server is a critical security hole. Anyone who guesses the path gets full read and write access to the database without a password. Always confirm that the folder is gone once the work is done!

The same applies to migration plugins: deactivate and remove them after the move if you no longer need them.


#How to change URLs in Elementor, Divi and Gutenberg

Modern page builders and the Gutenberg block editor add extra layers of data encoding that you need to remember when updating URLs.

graph LR
    subgraph WP_CLI_Execution["Step 1: database"]
        A["wp search-replace"] --> B["wp_posts, wp_postmeta, wp_options tables"]
    end

    subgraph Builder_Sync["Step 2: page builders"]
        B --> C["Elementor: Tools > Replace URL"]
        B --> D["Elementor: regenerate CSS files"]
    end

    subgraph Edge_Purge["Step 3: cache and edge"]
        C & D --> F["wp cache flush (Redis/Memcached)"]
        F --> G["Purge CDN (Cloudflare)"]
    end

#1. URLs in Elementor JSON data

Elementor stores page layouts in the _elementor_data meta field as JSON, where the slashes in URLs are often escaped:

{\"url\":\"https:\/\/old-domain.com\/wp-content\/uploads\/2026\/08\/banner.jpg\"}

A plain search for https://old-domain.com may not match the escaped https:\/\/old-domain.com. Elementor also compiles style rules into static CSS files in wp-content/uploads/elementor/css/.

The right procedure after migrating an Elementor site:

  1. Run the standard wp search-replace on the database.
  2. Log in to the WordPress dashboard and go to Elementor > Tools > Replace URL.
  3. Enter the old and new URL and click Replace URL.
  4. On the General tab click Regenerate Files & Data.

#2. URLs in Gutenberg blocks

Gutenberg blocks store media attributes directly in HTML comments inside post_content:

<!-- wp:image {"id":412,"sizeSlug":"full","linkDestination":"custom"} -->
<figure class="wp-block-image size-full"><a href="https://old-domain.com/offer/"><img src="https://old-domain.com/wp-content/uploads/2026/08/graphic.png" alt="Offer"/></a></figure>
<!-- /wp:image -->

Both wp search-replace and Better Search Replace handle this without trouble, as long as the post_content column of wp_posts is searched.

#3. URLs hard-coded in theme files

Some themes and plugins store URLs directly in PHP, CSS or JavaScript files. No database replacement reaches them, but they are easy to find on the server:

grep -rn "old-domain.com" wp-content/themes/your-theme/

#How to change the domain of a WooCommerce shop

When changing the domain of a WooCommerce shop, pay special attention to modules that talk to external payment systems, carriers and ERP systems:

  1. Payment gateways (Stripe, PayPal and others):
    • Most payment gateways register callback notification URLs (webhooks or IPN) in their developer dashboards. If you move from old.com to new.com, notifications about paid orders keep going to the old address and produce transaction errors.
    • After running wp search-replace, log in to each payment provider’s dashboard and update the webhook URL.
  2. WooCommerce REST API keys and webhooks:
    • In WooCommerce > Settings > Advanced > Webhooks check that the Delivery URLs have been updated. If webhooks sync stock levels with an ERP system, update the endpoint addresses in the integration software too.
  3. Sessions and customer carts:
    • The wp_woocommerce_sessions table stores temporary cart data. After a domain change it is worth clearing sessions in WooCommerce > Status > Tools > Clear customer sessions to avoid session token errors after the domain cookies change.

#How to run search-replace on a large database or MySQL cluster

On very large databases a single wp search-replace pass over all tables takes a long time, and for that whole time it writes to tables the live site is using. The WP-CLI documentation lists two limits worth knowing: tables without a primary key are skipped, and the --regex and --log options slow the run down considerably.

#1. How to split search-replace into smaller parts

wp search-replace has no --batch-size or --limit option; the option list in the WP-CLI documentation does not include them, and instructions that use them will not work. You split the work differently: pass table names as arguments (wildcards are allowed) or exclude tables with --skip-tables:

# Content first, then metadata, in separate runs
wp search-replace 'https://old.com' 'https://new.com' wp_posts --precise --skip-columns=guid
wp search-replace 'https://old.com' 'https://new.com' wp_postmeta --precise

# Everything except a large plugin log table
wp search-replace 'https://old.com' 'https://new.com' --all-tables --skip-tables='wp_*_log' --precise --skip-columns=guid

The --include-columns option narrows the replacement to the columns you list, and with --export the number of rows per INSERT statement is set by --export_insert_size (default 50).

#2. Search-replace with replication (Amazon Aurora, MySQL Galera)

If the database runs in a primary and replica setup or a Galera cluster, a bulk change to millions of rows produces a heavy stream of data in the binary log (binlog). That can cause significant replication lag on the read nodes.

Recommended approach for high-availability environments:

  • Run the operation in a maintenance window with the lowest traffic.
  • Temporarily detach the replica nodes for the bulk write.
  • Use the --export flag to prepare the processed SQL file on a separate work machine and import it during the maintenance window.

#3. Maintenance mode during the migration

WP-CLI can switch WordPress into maintenance mode, so visitors do not see a site that is halfway moved to the new address:

wp maintenance-mode activate
wp search-replace 'https://old.com' 'https://new.com' --all-tables --precise --skip-columns=guid
wp maintenance-mode deactivate

#How to switch WordPress from HTTP to HTTPS

If the goal of the migration is not a new domain but an SSL certificate and a switch from the unencrypted protocol to the encrypted one:

wp search-replace 'http://yourdomain.com' 'https://yourdomain.com' \
  --all-tables \
  --precise \
  --skip-columns=guid

This removes the mixed content problem, where the browser blocks scripts, fonts or images served over the old http:// protocol. Keep the domain in the search string: replacing a bare http:// would also rewrite links to other sites.

#301 redirects in Nginx and Apache

After changing the domain in the database, the critical SEO step is redirecting all traffic from the old address to the new one with HTTP status 301 (Moved Permanently).

Nginx configuration (/etc/nginx/sites-available/old-domain.conf):

server {
    listen 80;
    listen 443 ssl http2;
    server_name old-domain.com www.old-domain.com;

    ssl_certificate /etc/letsencrypt/live/old-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/old-domain.com/privkey.pem;

    return 301 https://new-domain.com$request_uri;
}

Apache configuration (.htaccess on the old server):

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{HTTP_HOST} ^(?:www\.)?old-domain\.com$ [NC]
    RewriteRule ^(.*)$ https://new-domain.com/$1 [L,R=301]
</IfModule>

To confirm the redirect and the new site, check the response codes: curl -s -o /dev/null -w "%{http_code}" https://old-domain.com/ should return 301, and the same command against the new domain 200.

#wp-config.php constants versus database values

A common emergency fix is forcing the URLs in wp-config.php:

define('WP_HOME', 'https://new-domain.com');
define('WP_SITEURL', 'https://new-domain.com');

How these constants work in practice:

  • Constants defined in wp-config.php override the siteurl and home values WordPress reads, but do not change the records in the database. The documentation calls this hard-coding the values and says it is not necessarily the best fix.
  • The address fields in Settings > General can no longer be edited.
  • Links in post content, attachments and widgets still point to the old address.
  • Treat the constants in wp-config.php as a temporary fix; the database itself should always be updated with wp search-replace.

#WordPress domain change checklist

Before you start, follow a structured plan:

StageTaskTool / command
Before migrationFull database dumpwp db export backup-$(date +%s).sql --add-drop-table
Before migrationCopy of wp-content/uploads and .htaccessrsync / tar -czf uploads.tar.gz
Before migrationDisable cache and minification pluginsWP dashboard / wp plugin deactivate
DuringDry run of the replacementwp search-replace 'old' 'new' --all-tables --dry-run
DuringReal replacement in the databasewp search-replace 'old' 'new' --all-tables --precise --skip-columns=guid
DuringReplace domains in builders (Elementor)Dashboard > Elementor > Tools > Replace URL
After migrationFlush object cache and transientswp cache flush and wp transient delete --all
After migrationRebuild permalinkswp rewrite flush --hard

#Bash script to sync a WordPress database between environments

In modern workflows built on Docker or local development and staging environments (DDEV, Lando, LocalWP), typing commands by hand is slow and error-prone. Engineering teams usually keep a reusable Bash script that pulls the production database, imports it locally, replaces the domains and anonymises sensitive customer data.

#!/usr/bin/env bash
# Automatic database sync script: sync-db.sh
set -eo pipefail

PROD_HOST="your-production-server.com"
PROD_USER="deploy"
PROD_PATH="/var/www/html"
PROD_URL="https://yourcompany.com"
LOCAL_URL="https://project.local"
BACKUP_DIR="./backups"

mkdir -p "${BACKUP_DIR}"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
DUMP_FILE="${BACKUP_DIR}/dump_prod_${TIMESTAMP}.sql"

echo "==> 1/4 Pulling the production database over SSH..."
ssh "${PROD_USER}@${PROD_HOST}" "cd ${PROD_PATH} && wp db export - --add-drop-table" > "${DUMP_FILE}"

echo "==> 2/4 Importing the database locally..."
wp db import "${DUMP_FILE}"

echo "==> 3/4 Replacing domains with serialization support..."
wp search-replace "${PROD_URL}" "${LOCAL_URL}" \
  --all-tables \
  --precise \
  --recurse-objects \
  --skip-columns=guid \
  --report-changed-only

echo "==> 4/4 Flushing caches and resetting permalinks..."
wp cache flush
wp rewrite flush --hard

echo "==> Done. The local environment is synced at ${LOCAL_URL}."

#How to verify serialization after the migration

To be certain that no field was damaged during the migration, run a short PHP snippet through WP-CLI that checks every serialized record in the wp_options table:

wp eval '
global $wpdb;
$options = $wpdb->get_results("SELECT option_name, option_value FROM {$wpdb->options} WHERE option_value LIKE \"a:%\" OR option_value LIKE \"O:%\"");
$broken = 0;
foreach ($options as $opt) {
    if (is_serialized($opt->option_value) && @unserialize($opt->option_value) === false) {
        echo "Broken option: " . $opt->option_name . PHP_EOL;
        $broken++;
    }
}
if ($broken === 0) {
    echo "All serialized options in wp_options are valid." . PHP_EOL;
} else {
    echo "Found " . $broken . " broken records!" . PHP_EOL;
}
'

To find old URLs left behind, count them directly, for example wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%old-domain.com%'", and the same for wp_postmeta.meta_value and wp_options.option_value.


#WordPress errors after a domain change and how to fix them

#1. Redirect loop after a domain change (ERR_TOO_MANY_REDIRECTS)

If the site falls into an endless redirect loop after the domain change:

  • Check that siteurl and home in wp_options use exactly the same protocol (https://) and prefix (www or bare domain) as the web server or Cloudflare is configured for.
  • Check the SSL setting in Cloudflare (SSL/TLS mode should be Full (Strict), not Flexible).
  • Reset .htaccess to the WordPress default with:
    wp rewrite flush --hard

#2. CSS styles and icons missing after migration

  • If the page loads as raw text without styles, the usual causes are stylesheets and scripts still pointing to the old address, mixed content after the move to HTTPS, or resources blocked by a CORS policy.
  • Open the browser developer tools (F12 > Console tab) to find files loaded from the old http:// address.

#3. Broken widgets or ACF fields

  • If the serialization rules were ignored and a raw SQL REPLACE was run, the only safe way out is restoring the database from the earlier dump (wp db import backup.sql) and repeating the operation with wp search-replace --precise.

#4. Allowed memory size exhausted in WP-CLI

During search-replace on tables with large amounts of text (for example big log tables or post revisions), the PHP process in the terminal can run out of its memory limit:

  • To raise the memory limit for a single WP-CLI command, run it with a PHP setting:
    php -d memory_limit=1024M /usr/local/bin/wp search-replace 'https://old.com' 'https://new.com' --all-tables
  • It also helps to delete unneeded post revisions and expired transients before the migration:
    wp transient delete --all && wp post delete $(wp post list --post_type='revision' --format=ids) --force

#5. Character set and collation mismatch

When moving a database dump to an older or different database server, the target server may not know the collation used in the dump, for example utf8mb4_unicode_520_ci:

  • If the import of the .sql file fails with #1273 - Unknown collation: 'utf8mb4_unicode_520_ci', replace that string in the SQL file before importing:
    sed -i 's/utf8mb4_unicode_520_ci/utf8mb4_unicode_ci/g' backup.sql
  • Both collations store utf8mb4, so accented characters are kept; only the sorting and comparison rule changes.

#Summary

Updating URLs in a WordPress database correctly requires knowing how PHP and relational databases handle the data. Raw SQL queries or editing dump files in a text editor are the shortest route to a broken site and lost configuration.

WP-CLI with the flags --all-tables --precise --skip-columns=guid, preceded by a backup and a --dry-run pass, keeps serialized data and the guid column intact.

Planning a full rebuild, a move to a headless architecture or a migration to a newer platform? See our services for migrating from WordPress to Astro and Next.js and the dedicated Core Web Vitals audit.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If visibility in Google and AI systems matters, I can build the content architecture, FAQ, schema, and internal linking needed for SEO, GEO, and AEO.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Why does a plain SQL UPDATE with REPLACE break a WordPress database#
WordPress stores complex configuration, widgets, theme settings and ACF fields as serialized PHP arrays, for example s:15:"https://old.com". SQL REPLACE swaps the text but does not update the length counter. When the data is read back, PHP sees the length mismatch and rejects the whole structure as corrupted.
What is the safest tool for changing the domain in a WordPress database#
The most recommended tool is the official WP-CLI with the wp search-replace command. According to the WP-CLI documentation it handles PHP serialized data correctly: it unserializes the data, replaces the text, recalculates the lengths and writes it back.
Should I change URLs in the guid column of the wp_posts table#
No. The guid column gives feed readers a permanent, unique identifier for each post. The WordPress documentation says the GUID column must never be changed, which is why WP-CLI is run with --skip-columns=guid.
How do I update URLs on shared hosting without SSH access#
Without SSH, the WordPress documentation points to the Better Search Replace plugin (maintained by WP Engine) or the Search and Replace for WordPress Databases script by Interconnect/it. Delete the script from the server as soon as the work is done.
What should I do if widgets disappear after migration#
Missing widgets mean the serialized data was corrupted. Restore the backup with wp db import backup.sql, then run the migration again with a serialization-aware tool such as WP-CLI or Better Search Replace.
Do I need to update URLs when moving from HTTP to HTTPS#
Yes. Replace http://yourdomain.com with https://yourdomain.com across all tables to avoid mixed content warnings. Scope the replacement to your own domain so links to other sites are left alone.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles

WordPress Hosting Benchmarks 2026

Kevin Ohashi's Review Signal benchmarks returned in 2026 after a three-year gap, powered by a brand-new open-source load-testing platform. We break down the five price tiers plus WooCommerce, name the winners, name the hosts who sat it out, and translate the data for European agencies.