Back to Blog
Platform Guides

Migrating WordPress Between Managed Hosts: The Platform-Aware Technical Guide

Sarah Kim
39 min read

Why Managed Host Migrations Are Different

Migrating a WordPress site between two shared hosting accounts is straightforward. You export the database, copy the files, update wp-config.php, and run a search-replace. Fifteen minutes, done. Migrating between managed WordPress hosts is a different undertaking entirely.

Managed hosts like WP Engine, Kinsta, Pantheon, WordPress VIP, and Cloudways each impose their own architecture on your WordPress installation. They inject must-use plugins, enforce specific caching layers, modify the file system structure, and sometimes lock down core WordPress behaviors. When you move from one managed host to another, you are not just moving files and data. You are untangling one platform’s assumptions and re-wiring your site to operate under a completely different set of rules.

I have personally handled over 200 managed-host-to-managed-host migrations in my career. Every single one required platform-specific knowledge that generic migration guides simply do not cover. This article is the guide I wish I had when I started. It is long, it is detailed, and it will save you hours of debugging if you read it before touching your production DNS records.

Pre-Migration Audit: Mapping Platform Dependencies

Before you export a single file, you need a clear picture of how deeply your current managed host has embedded itself into your WordPress installation. Skip this step and you will spend your post-migration hours chasing phantom errors that all trace back to the same root cause: leftover platform code trying to talk to infrastructure that no longer exists.

Identifying Must-Use Plugins

Every managed WordPress host installs plugins in the wp-content/mu-plugins/ directory. Unlike regular plugins, mu-plugins load automatically on every page request and cannot be deactivated through the WordPress admin. They are the primary mechanism hosts use to integrate their platform features with your WordPress installation.

Start by listing what is in that directory:

ls -la wp-content/mu-plugins/

On WP Engine, you will typically find:

mu-plugin.php
wpengine-common/
force-strong-passwords.php
stop-long-comments.php

On Pantheon:

pantheon-mu-plugin/
pantheon.php

On Kinsta:

kinsta-mu-plugins/
kinsta-mu-plugins.php

Document every file and directory. Then check what each one does by reading its headers:

head -20 wp-content/mu-plugins/*.php

Some mu-plugins are harmless (like force-strong-passwords.php on WP Engine), but others are deeply integrated with the host’s caching layer, CDN, or security infrastructure. Those are the ones that will cause problems on the new host if left behind, and that will cause problems on the current host if removed prematurely.

Mapping Cache Dependencies

Managed hosts implement caching at multiple levels, and your WordPress installation may be configured to interact with those layers through drop-in plugins. Check for these files:

ls -la wp-content/object-cache.php
ls -la wp-content/advanced-cache.php
ls -la wp-content/db.php

The object-cache.php drop-in is especially important. It defines how WordPress stores and retrieves transient data, query results, and other cached objects. WP Engine provides a Memcached-based object cache. Kinsta uses Redis. Pantheon uses its own object cache integration. If you move the old host’s object-cache.php to the new host, your site will attempt to connect to a caching backend that does not exist, resulting in either fatal errors or silently degraded performance.

Run this command to see which caching constants are defined in your configuration:

wp config list --fields=name,value | grep -i cache

Common constants to watch for:

WP_CACHE
WP_CACHE_KEY_SALT
KINSTA_CACHE_ZONE
WP_REDIS_HOST
WP_REDIS_PORT
MEMCACHED_SERVERS

Checking for Host-Specific Code in Themes and Plugins

Developers sometimes write code that targets a specific hosting environment. Search your theme and plugin files for host-specific references:

grep -r "WPE_APIKEY\|IS_WPE\|KINSTA\|PANTHEON_ENVIRONMENT\|WPCOMVIP" wp-content/themes/ wp-content/plugins/ --include="*.php" -l

If this returns results, open each file and evaluate whether the host-specific code will fail gracefully on the new platform or whether it will throw errors. Conditional checks like if (defined('IS_WPE')) are usually safe because they simply will not execute on a non-WP-Engine host. Direct calls to host-specific functions like WpeCommon::purge_memcached() will cause fatal errors.

Documenting Cron and Scheduled Tasks

Managed hosts often replace WordPress’s built-in wp-cron with their own server-level cron implementation. Check whether wp-cron is disabled:

wp config get DISABLE_WP_CRON

If it returns true, the host is handling cron externally. Your new host may or may not do the same. List all scheduled cron events so you can verify they resume correctly after migration:

wp cron event list --fields=hook,next_run_relative,recurrence

Save this output. You will use it during post-migration validation.

WP Engine to Kinsta: A Step-by-Step Migration

This is one of the most common managed-host migrations, and it has several specific gotchas. WP Engine and Kinsta both offer premium managed WordPress hosting, but their underlying architectures differ significantly.

Step 1: Export from WP Engine

WP Engine offers SFTP access and a backup download feature. For a clean migration, use SFTP rather than the backup ZIP, because the backup format includes WP Engine metadata that you do not need.

Connect via SFTP and download the entire site:

sftp [email protected]
get -r /sites/your-install/

Alternatively, use rsync if your WP Engine plan supports SSH:

rsync -avz --progress -e "ssh -p 2222" [email protected]:/sites/your-install/ ./local-backup/

Export the database separately using WP-CLI through WP Engine’s SSH gateway:

ssh [email protected] "cd /sites/your-install && wp db export --single-transaction - " > wpe-database.sql

The --single-transaction flag ensures a consistent snapshot without locking tables.

Step 2: Remove WP Engine mu-plugins

Before uploading anything to Kinsta, remove all WP Engine must-use plugins:

rm -rf wp-content/mu-plugins/mu-plugin.php
rm -rf wp-content/mu-plugins/wpengine-common/
rm -rf wp-content/mu-plugins/force-strong-passwords.php
rm -rf wp-content/mu-plugins/stop-long-comments.php
rm -rf wp-content/mu-plugins/slt-force-strong-passwords.php

Also remove the WP Engine object cache drop-in:

rm wp-content/object-cache.php

This file connects to WP Engine’s Memcached servers. Leaving it in place on Kinsta will cause a white screen of death because the Memcached endpoints will not resolve.

Check wp-config.php for WP Engine-specific constants and remove them:

# Remove these lines if present:
# define('WPE_APIKEY', 'your-key-here');
# define('WPE_CLUSTER_ID', '12345');
# define('WPE_ISP', true);
# define('PWP_NAME', 'your-install');

Step 3: Configure for Kinsta

Kinsta uses Redis for object caching. After uploading your files to Kinsta via SFTP, install the Redis object cache through the Kinsta dashboard (MyKinsta > Sites > Your Site > Tools > Redis Cache). Kinsta will automatically place the correct object-cache.php drop-in.

Kinsta also requires specific constants in wp-config.php. Their provisioning process usually handles this, but verify these are present:

define('WP_CACHE', true);
define('WP_CACHE_KEY_SALT', 'your-site-name_');
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);

Step 4: Import the Database

Upload your database export to Kinsta and import it:

wp db import wpe-database.sql

Then run the search-replace to update URLs. WP Engine uses your your-install.wpengine.com staging URL, and you need to replace it with your actual domain or Kinsta’s temporary URL:

wp search-replace 'your-install.wpengine.com' 'your-site.kinsta.cloud' --all-tables --precise --recurse-objects

The --recurse-objects flag is critical. It handles serialized data in options and postmeta, which would otherwise be corrupted by a naive find-and-replace. More on this in the database migration section below.

Step 5: DNS Switchover

Kinsta uses Cloudflare-based DNS integration through their dashboard. Point your domain’s A record to the IP address shown in MyKinsta, or use Kinsta’s DNS service directly. If you are using a root domain (no www prefix), you will need either an A record or a CNAME-flattening-capable DNS provider.

The TTL on your existing DNS records matters. If your current TTL is 86400 (24 hours), reduce it to 300 (5 minutes) at least 48 hours before the migration. This ensures the DNS change propagates quickly when you make the switch.

# Check current TTL
dig +noall +answer yourdomain.com

# After reducing TTL and waiting for propagation, update DNS
# Then verify propagation:
dig +short yourdomain.com @8.8.8.8
dig +short yourdomain.com @1.1.1.1

Pantheon to WP Engine: Handling the File System Differences

Pantheon’s architecture is fundamentally different from most WordPress hosts. It uses a Git-based deployment workflow, separates code from files and database, and runs on a container-based infrastructure with a read-only file system for code. Moving from Pantheon to WP Engine requires understanding these architectural differences.

Understanding Pantheon’s Structure

On Pantheon, your WordPress installation is split across three components:

Code (version-controlled via Git): WordPress core, themes, plugins, mu-plugins.

Files (stored separately): The wp-content/uploads/ directory, which is writable but not in the Git repository.

Database: Stored on Pantheon’s database cluster.

This means you cannot simply copy the entire WordPress directory as a unit. You need to pull code, files, and database separately.

Step 1: Pull Code from Pantheon

Clone the Pantheon Git repository:

terminus connection:info your-site.live --field=git_url
git clone ssh://[email protected]:2222/~/repository.git pantheon-code

Alternatively, use Terminus to create a backup and download it:

terminus backup:create your-site.live --element=code
terminus backup:get your-site.live --element=code --to=./code-backup.tar.gz

Step 2: Pull Files (Uploads)

The uploads directory is separate from the code repository on Pantheon. Download it using rsync:

terminus rsync your-site.live:files/ ./uploads/

Or use Terminus backup commands:

terminus backup:create your-site.live --element=files
terminus backup:get your-site.live --element=files --to=./files-backup.tar.gz

Step 3: Export the Database

terminus backup:create your-site.live --element=db
terminus backup:get your-site.live --element=db --to=./database-backup.sql.gz
gunzip database-backup.sql.gz

Step 4: Remove Pantheon mu-plugins

Pantheon installs its own mu-plugin that handles edge caching integration, SSO, and other platform features. Remove it:

rm -rf wp-content/mu-plugins/pantheon-mu-plugin/
rm -f wp-content/mu-plugins/pantheon.php

Also check for and remove Pantheon’s wp-content/object-cache.php if present:

rm -f wp-content/object-cache.php

Pantheon defines several environment-specific constants. Remove or update these in wp-config.php:

# Remove Pantheon-specific code blocks like:
# if (isset($_ENV['PANTHEON_ENVIRONMENT'])) { ... }
# define('PANTHEON_ENVIRONMENT', 'live');

Pantheon also uses a modified wp-config.php that sources database credentials from environment variables. On WP Engine, database credentials are provided through their own mechanism. You will need to replace Pantheon’s dynamic config with WP Engine’s standard format:

define('DB_NAME', 'wp_your_install');
define('DB_USER', 'your_install');
define('DB_PASSWORD', '');  // WP Engine handles this
define('DB_HOST', 'localhost');

WP Engine will overwrite certain wp-config.php values at runtime, so the exact credentials are less important than getting the structure correct.

Step 5: Reassemble and Upload to WP Engine

Combine the code and uploads into a standard WordPress directory structure:

cp -r uploads/* pantheon-code/wp-content/uploads/

Then SFTP the entire directory to WP Engine:

sftp [email protected]
put -r pantheon-code/* /sites/your-install/

Import the database through WP Engine’s phpMyAdmin or via SSH:

ssh [email protected]
cd /sites/your-install
wp db import /path/to/database-backup.sql
wp search-replace 'live-your-site.pantheonsite.io' 'your-install.wpengine.com' --all-tables --precise --recurse-objects

WordPress VIP Outbound: Untangling the Deepest Integration

WordPress VIP is the most opinionated managed WordPress platform. Moving away from VIP is the most complex migration you will encounter, and it requires careful planning. VIP’s codebase assumptions run deep, and their mu-plugin layer provides functionality that many themes and plugins depend on without the developer realizing it.

Understanding VIP’s Architecture

VIP Go (the current platform) runs WordPress on a containerized infrastructure with:

  • A proprietary mu-plugin suite (vip-go-mu-plugins) providing caching, image handling, search, and more
  • A custom file service (VIP Files) that replaces standard local uploads
  • Elasticsearch-powered search replacing the default MySQL-based WordPress search
  • A CDN (Fastly) with tight integration via the mu-plugins
  • Custom caching functions that abstract away the cache backend

Step 1: Audit VIP Dependencies in Your Theme

VIP themes often call VIP-specific functions directly. Search for these:

grep -rn "wpcom_vip_\|vip_safe_wp_remote_get\|vip_get_env_var\|jetpack_\|A8C\\" wp-content/themes/your-theme/ --include="*.php"

Common VIP functions that will cause fatal errors on other hosts:

  • wpcom_vip_load_plugin() – VIP’s plugin loading mechanism
  • vip_safe_wp_remote_get() – Rate-limited HTTP wrapper
  • wpcom_vip_file_get_contents() – Cached remote file fetching
  • wpcom_vip_attachment_url_to_postid() – Optimized media lookup

For each function call you find, you need to either replace it with a standard WordPress equivalent or write a compatibility shim. Here is a basic shim file you can place in mu-plugins/vip-compat-shim.php:

<?php
/**
 * VIP Compatibility Shim
 * Provides fallbacks for VIP-specific functions after migration.
 */

if ( ! function_exists( 'wpcom_vip_load_plugin' ) ) {
    function wpcom_vip_load_plugin( $plugin_name, $folder = '' ) {
        // On VIP, this loaded plugins from a shared directory.
        // Off VIP, just return false and ensure plugins are installed normally.
        return false;
    }
}

if ( ! function_exists( 'vip_safe_wp_remote_get' ) ) {
    function vip_safe_wp_remote_get( $url, $args = array() ) {
        return wp_remote_get( $url, $args );
    }
}

if ( ! function_exists( 'wpcom_vip_file_get_contents' ) ) {
    function wpcom_vip_file_get_contents( $url, $timeout = 3 ) {
        $response = wp_remote_get( $url, array( 'timeout' => $timeout ) );
        if ( is_wp_error( $response ) ) {
            return false;
        }
        return wp_remote_retrieve_body( $response );
    }
}

if ( ! function_exists( 'wpcom_vip_attachment_url_to_postid' ) ) {
    function wpcom_vip_attachment_url_to_postid( $url ) {
        return attachment_url_to_postid( $url );
    }
}

Step 2: Handle VIP Files (Media)

VIP stores uploaded media on a separate file service, not in the local wp-content/uploads/ directory. URLs for media on VIP look like:

https://your-site.go-vip.net/wp-content/uploads/2022/07/image.jpg

But the files do not actually live on disk at that path. The VIP file service intercepts the request and serves the file from object storage. When migrating away from VIP, you need to:

1. Download all media files from VIP’s file service
2. Place them in the standard uploads directory structure
3. Update database references to point to the new URL structure

Use the VIP CLI to sync files:

vip @your-site.production -- wp vip files sync --dest=./local-uploads/

If that is not available, you can crawl and download the uploads using wget:

wget --mirror --no-parent --cut-dirs=3 -P ./local-uploads/ https://your-site.go-vip.net/wp-content/uploads/

Step 3: Replace Elasticsearch with Standard Search

If your site uses VIP’s Elasticsearch integration (and most VIP sites do), you need to decide on a replacement. Options include:

  • Standard WordPress search: No changes needed, but search quality drops significantly
  • ElasticPress plugin: If your new host offers Elasticsearch, this plugin provides similar functionality
  • Algolia: A SaaS search solution with a WordPress plugin
  • SearchWP: A premium plugin that improves MySQL-based search without requiring external services

Remove VIP search integration code from your theme:

grep -rn "Enterprise_Search\|VIP_Search\|ep_integrate\|elasticsearch" wp-content/themes/your-theme/ --include="*.php" -l

Step 4: Cache API Migration

VIP’s caching layer provides functions that your theme may depend on. The most common is their advanced page caching with cache segmentation. Search for:

grep -rn "wpcom_vip_load_category_cache\|wpcom_vip_load_permastruct\|batcache\|advanced-cache" wp-content/themes/ --include="*.php" -l

On your new host, you will need to configure whatever caching mechanism that platform provides. The specific steps depend on your destination host.

Cloudways to Managed Host: Breeze and Varnish Migration

Cloudways occupies an interesting middle ground between unmanaged cloud servers and fully managed WordPress hosting. It provides a management layer on top of infrastructure from providers like DigitalOcean, Vultr, AWS, and Google Cloud. When migrating from Cloudways to a managed host, you need to remove Cloudways-specific optimizations and replace them with whatever your new host provides.

Step 1: Remove the Breeze Plugin

Cloudways installs their Breeze caching plugin by default. Breeze handles page caching, browser caching, database optimization, and CDN integration. Before migrating, deactivate and remove it:

wp plugin deactivate breeze
wp plugin delete breeze

Breeze also modifies your .htaccess file (on Apache servers) or creates Nginx configuration rules. If your Cloudways server uses Apache, check .htaccess for Breeze-related rules:

# Look for blocks between these markers:
# BEGIN Breeze
# ... rules ...
# END Breeze

Remove those blocks entirely. Your new managed host will handle caching at the server level.

Step 2: Varnish Configuration

Cloudways uses Varnish as a reverse proxy cache on most server configurations. If you have custom Varnish rules (VCL), document them because you may need to replicate their behavior on the new host. Check for custom purge rules in your theme or plugins:

grep -rn "varnish\|PURGE\|purge_url\|do_action.*cache_purge" wp-content/themes/ wp-content/plugins/ --include="*.php" -l

Most managed hosts do not expose Varnish configuration directly. WP Engine uses their own caching layer (EverCache). Kinsta uses Nginx-based caching with a custom purge mechanism. You will need to replace any custom Varnish purge code with the appropriate cache purge mechanism for your destination host.

For WP Engine, cache purging is handled through their mu-plugin:

// WP Engine cache purge
if ( class_exists( 'WpeCommon' ) ) {
    WpeCommon::purge_memcached();
    WpeCommon::purge_varnish_cache();
}

For Kinsta, cache purging goes through their API or mu-plugin:

// Kinsta cache purge
if ( isset( $kinsta_cache ) ) {
    $kinsta_cache->kinsta_cache_purge->purge_complete_caches();
}

Step 3: Handle Cloudways-Specific wp-config Settings

Cloudways adds several constants to wp-config.php:

# Remove or update these:
# define('STARTER_STARTER', 'developer');
# define('STARTER_APP', 'starter');
# define('STARTER_FLAVOR', 'developer');

Also check for Redis or Memcached configuration that was set up through Cloudways:

wp config list --fields=name,value | grep -iE "redis|memcache"

Remove any Cloudways-specific Redis configuration and let your new host configure its own object caching.

Step 4: Database and Files Export

Export the database from Cloudways:

wp db export cloudways-backup.sql --single-transaction

For files, use SFTP or rsync:

rsync -avz --progress user@your-server-ip:/home/master/applications/your-app/public_html/ ./cloudways-backup/

Database Migration: Search-Replace Strategies and Serialized Data

The database is where most migration problems hide. WordPress stores URLs, file paths, and configuration data throughout its database, and a simple SQL find-and-replace will corrupt serialized data. This section covers the correct approach.

Understanding Serialized Data

WordPress uses PHP serialization to store complex data structures (arrays and objects) in database columns that only support string values. A serialized string looks like this:

a:2:{s:4:"site";s:24:"old-site.wpengine.com";s:4:"path";s:22:"/var/www/old-site/";}

Notice the s:24: prefix before the URL. That is a byte-length prefix; it tells PHP that the following string is exactly 24 characters long. If you change the URL to new-site.kinsta.cloud (21 characters) without updating the length prefix to s:21:, PHP will fail to unserialize the data. The result: broken widgets, lost theme settings, corrupted plugin options, and mysterious errors that are difficult to trace.

WP-CLI Search-Replace: The Safe Method

WP-CLI’s search-replace command handles serialized data correctly. It unserializes the data, performs the replacement, recalculates the length prefixes, and re-serializes. Always use it:

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

Key flags explained:

  • --all-tables: Searches all tables, not just core WordPress tables. Essential if you use plugins that create custom tables.
  • --precise: Uses PHP’s serialization functions for exact replacements rather than regex approximations.
  • --recurse-objects: Handles nested serialized data (serialized arrays within serialized arrays).
  • --report-changed-only: Only shows tables where replacements were actually made, keeping output clean.

Multi-Pass Replacement Strategy

A single search-replace pass is rarely sufficient for managed host migrations. You need to replace multiple URL variants and paths. Run replacements in this order:

# Pass 1: HTTPS URLs
wp search-replace 'https://old-domain.com' 'https://new-domain.com' --all-tables --precise --recurse-objects

# Pass 2: HTTP URLs (in case any exist)
wp search-replace 'http://old-domain.com' 'https://new-domain.com' --all-tables --precise --recurse-objects

# Pass 3: Protocol-relative URLs
wp search-replace '//old-domain.com' '//new-domain.com' --all-tables --precise --recurse-objects

# Pass 4: Staging/temporary URLs from the old host
wp search-replace 'your-site.wpengine.com' 'new-domain.com' --all-tables --precise --recurse-objects

# Pass 5: File paths (if they changed between hosts)
wp search-replace '/srv/www/old-path' '/var/www/new-path' --all-tables --precise --recurse-objects

Dry Run First

Always run a dry run before executing the actual replacement:

wp search-replace 'old-domain.com' 'new-domain.com' --all-tables --precise --recurse-objects --dry-run

This shows you exactly which tables will be affected and how many replacements will be made, without actually modifying any data. Review the output carefully. If you see an unexpectedly high number of replacements in a table, investigate before proceeding.

Handling Large Databases

For databases over 1 GB, the standard search-replace can be slow or run out of memory. Use the --skip-columns flag to exclude large text columns that you know do not contain URLs:

wp search-replace 'old-domain.com' 'new-domain.com' wp_posts --skip-columns=post_password --precise --recurse-objects

You can also process tables individually and in batches:

# Get list of tables
wp db tables --all-tables

# Process each table separately
for table in $(wp db tables --all-tables --format=csv); do
    echo "Processing $table..."
    wp search-replace 'old-domain.com' 'new-domain.com' "$table" --precise --recurse-objects
done

Verifying the Replacement

After running search-replace, verify that no old URLs remain:

wp db search 'old-domain.com' --all-tables --stats

If old URLs persist, they may be in tables or columns that were not covered. Check for base64-encoded content or JSON-encoded URLs that the search-replace did not catch:

wp db query "SELECT option_name, option_value FROM wp_options WHERE option_value LIKE '%old-domain.com%'" --skip-column-names

DNS and SSL: Platform-Specific Provisioning

DNS and SSL configuration varies significantly between managed hosts. Getting this wrong causes downtime, mixed content warnings, and certificate errors. Plan this step carefully.

Pre-Migration DNS Preparation

At least 48 hours before the migration, reduce the TTL on your DNS records:

# Log in to your DNS provider and set TTL to 300 seconds (5 minutes)
# for A records and CNAME records pointing to your site

# Verify the TTL change has propagated:
dig +noall +answer yourdomain.com
# Look for the TTL value (second column); it should be 300 or close to it

SSL Provisioning by Platform

Each managed host handles SSL differently:

WP Engine: Provides free Let’s Encrypt certificates. You can also install custom certificates. SSL is provisioned through the WP Engine User Portal after adding your domain. Important: WP Engine cannot issue the certificate until DNS points to their servers, creating a brief window where your site is accessible but without a valid certificate.

Kinsta: Also uses Let’s Encrypt, provisioned through MyKinsta. Kinsta’s Cloudflare integration can provide an SSL certificate before DNS fully propagates if you use their DNS service. This eliminates the gap.

Pantheon: Provides free Let’s Encrypt certificates through their Global CDN. Certificates auto-provision after DNS verification. Pantheon also supports custom certificates on higher-tier plans.

Cloudways: Integrates Let’s Encrypt through their dashboard. You click a button, enter your domain and email, and the certificate is issued within minutes, provided DNS already points to the Cloudways server.

Minimizing Downtime During DNS Switchover

The standard approach is to set up the new site, verify everything works on the temporary URL, then switch DNS. But there is a gap between when DNS propagates and when the new host can issue an SSL certificate. Here are strategies to minimize that gap:

Strategy 1: Pre-provision SSL with DNS validation

Some hosts support DNS-based SSL validation. Instead of proving domain ownership via HTTP (which requires DNS to already point to the new host), you add a DNS TXT record. This lets you provision the SSL certificate before switching the A/CNAME records:

# Add a TXT record for SSL validation (exact format depends on the host)
_acme-challenge.yourdomain.com  TXT  "validation-string-from-host"

# After certificate is issued, then update A/CNAME records

Strategy 2: Use Cloudflare as an intermediary

Place Cloudflare in front of your domain. Cloudflare provides its own SSL certificate (via Universal SSL) and proxies traffic to whatever backend you specify. During migration:

1. Set up Cloudflare, point DNS to Cloudflare nameservers.
2. Configure Cloudflare to proxy to your old host.
3. Set up the new host with a temporary URL and verify everything works.
4. Update Cloudflare’s origin IP to point to the new host.
5. Visitors never see downtime because Cloudflare’s SSL certificate never changes.

# In Cloudflare, update the origin:
# A record: yourdomain.com -> new-host-ip (proxied through Cloudflare)

# Verify Cloudflare is routing to the new host:
curl -sI https://yourdomain.com | grep -i server

CDN Cache Warming

After DNS switches to the new host, your CDN cache is cold. Every request hits the origin server directly until the CDN caches the response. For high-traffic sites, this can overwhelm the origin server.

Warm the cache by crawling your site immediately after DNS propagation:

# Generate a list of URLs from the sitemap
curl -s https://yourdomain.com/sitemap.xml | grep -oP '(?<=).*?(?=)' > urls.txt

# Crawl each URL to warm the cache
while IFS= read -r url; do
    curl -s -o /dev/null -w "%{http_code} %{url_effective}\n" "$url"
    sleep 0.5  # Be gentle on the origin
done < urls.txt

For sites with thousands of pages, use a parallel crawler:

cat urls.txt | xargs -P 10 -I {} curl -s -o /dev/null -w "%{http_code} {}\n" {}

Handling Uploads and Media Files Across Platforms

Media files are often the largest part of a WordPress site, and their handling varies significantly across managed hosts. A site with 50 GB of uploads requires different strategies than one with 500 MB.

Verifying File Integrity

Before starting the migration, generate a manifest of all files with checksums:

find wp-content/uploads/ -type f -exec md5sum {} \; > uploads-manifest.txt
wc -l uploads-manifest.txt  # Count total files

After copying files to the new host, verify the manifest:

md5sum -c uploads-manifest.txt | grep -v "OK$"

Any files that do not match indicate a corruption during transfer.

Transferring Large Media Libraries

For sites with large uploads directories (over 5 GB), SFTP is painfully slow. Use rsync over SSH whenever possible:

rsync -avz --progress --checksum \
    ./wp-content/uploads/ \
    user@new-host:/path/to/wp-content/uploads/

The --checksum flag uses file checksums instead of timestamps to determine which files need transfer. This is slower for the initial scan but ensures accuracy.

If the destination host does not support SSH/rsync, consider compressing the uploads first:

# Split into multiple archives if the total is very large
tar czf uploads-2020.tar.gz wp-content/uploads/2020/
tar czf uploads-2021.tar.gz wp-content/uploads/2021/
tar czf uploads-2022.tar.gz wp-content/uploads/2022/

# Upload and extract on the new host
scp uploads-*.tar.gz user@new-host:/path/to/
ssh user@new-host "cd /path/to/wp-content/ && for f in /path/to/uploads-*.tar.gz; do tar xzf \$f; done"

Handling Platform-Specific Media URLs

Some managed hosts serve media through their own CDN with a different URL structure. For example:

  • WP Engine: May use your-install.wpengine.com/wp-content/uploads/...
  • Pantheon: Uses live-your-site.pantheonsite.io/wp-content/uploads/...
  • VIP: Uses your-site.go-vip.net/wp-content/uploads/... or a custom files domain
  • Cloudways: May use a StackPath or Cloudflare CDN URL

All of these references exist in your database, embedded in post content, metadata, widget settings, and theme options. The search-replace passes described in the database section handle most of them, but verify by querying the database directly:

wp db query "SELECT ID, guid FROM wp_posts WHERE post_type='attachment' AND guid LIKE '%old-host-url%' LIMIT 10"

Note that the guid column for attachments should generally not be changed, as WordPress uses it as a unique identifier, not as a URL for serving files. The _wp_attached_file and _wp_attachment_metadata postmeta fields are what actually matters:

wp db query "SELECT post_id, meta_value FROM wp_postmeta WHERE meta_key='_wp_attached_file' LIMIT 10"

Regenerating Thumbnails

If your old host and new host use different image processing libraries (ImageMagick vs. GD, or different versions), the generated thumbnails may differ. After migration, regenerate thumbnails to ensure consistency:

wp media regenerate --yes

For large media libraries, this can take hours. Run it in the background or use the --only-missing flag to only generate thumbnails that do not already exist:

wp media regenerate --only-missing --yes

Post-Migration Validation Checklist

After completing the technical migration steps, you need to verify that everything works correctly. Do not skip this. I have seen migrations that appeared successful but had subtle issues that did not surface until days later, causing SEO penalties, broken forms, and lost revenue.

Core Functionality Checks

Run these WP-CLI commands immediately after migration:

# Verify WordPress core checksums
wp core verify-checksums

# Check for database issues
wp db check

# Flush all caches
wp cache flush

# Flush rewrite rules
wp rewrite flush

# Verify all plugins are active and no errors
wp plugin list --status=active --fields=name,version,status

# Verify theme
wp theme list --status=active --fields=name,version,status

# Check for PHP errors in recent log
tail -50 /path/to/php-error.log

URL and Content Verification

# Check homepage loads correctly
curl -sI https://yourdomain.com | head -20

# Verify SSL certificate
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -dates -issuer

# Check for mixed content (HTTP resources on HTTPS pages)
curl -s https://yourdomain.com | grep -i "http://" | grep -v "https://"

# Check for old host references in the HTML output
curl -s https://yourdomain.com | grep -i "old-host-url"

# Verify sitemap accessibility
curl -sI https://yourdomain.com/sitemap.xml

# Check robots.txt
curl -s https://yourdomain.com/robots.txt

Database Verification

# Confirm no old URLs remain in the database
wp db search 'old-host-url' --all-tables --stats

# Verify options table has correct URLs
wp option get siteurl
wp option get home

# Check for orphaned metadata
wp db query "SELECT COUNT(*) FROM wp_postmeta WHERE meta_value LIKE '%old-host-url%'"

Cron Verification

Compare the cron event list you saved during pre-migration with the current state:

wp cron event list --fields=hook,next_run_relative,recurrence

If events are missing or have incorrect schedules, re-register them:

# Test that cron is actually running
wp cron test

If wp-cron is disabled (as it often is on managed hosts), verify that the host's server-level cron is triggering wp-cron.php at the expected interval.

Performance Baseline

Run a quick performance check to establish a baseline on the new host:

# Time to first byte (TTFB) - test from command line
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://yourdomain.com

# Check WordPress performance with Query Monitor or similar
wp plugin install query-monitor --activate

# Run a basic load test (be careful, only on staging or with low concurrency)
# ab -n 100 -c 5 https://yourdomain.com/

Email Verification

Test that WordPress can send email from the new host:

wp eval "wp_mail('[email protected]', 'Migration Test', 'Email from new host works.');"

Many managed hosts restrict outbound email. If this test fails, you may need to configure an SMTP plugin (WP Mail SMTP, FluentSMTP, or similar) with a transactional email service like Mailgun, SendGrid, or Amazon SES.

The Visual Check

Automated checks catch technical problems, but they cannot catch visual regressions. Open the following pages in a browser and compare them to screenshots or cached versions from the old host:

  • Homepage
  • A blog post with images
  • A page with forms
  • The login and registration pages
  • The site on mobile (use browser DevTools responsive mode)
  • Any page that uses JavaScript-heavy features (sliders, accordions, interactive elements)

Pay special attention to web fonts. If your old host served fonts from a CDN or used a specific font loading strategy, fonts may render differently or not at all on the new host.

Rollback Strategies When Migrations Go Wrong

Not every migration goes smoothly. You need a plan for when things break, and you need that plan in place before you start, not after you discover the problem at 2 AM on a Saturday.

Strategy 1: DNS-Based Rollback (Fastest)

The simplest rollback method is to switch DNS back to the old host. This is why you keep the old host running until you have fully validated the new one.

Prerequisites for this to work:

  • The old host is still active and serving the site correctly
  • You have not made changes to the site on the new host that you need to preserve
  • Your DNS TTL is low (300 seconds or less)

To roll back, simply update your DNS records to point back to the old host's IP address. Within 5 to 10 minutes (depending on TTL), traffic will route back to the old host.

# Point DNS back to old host
# In your DNS provider, update A record to old host IP

# Verify DNS is resolving to old host
dig +short yourdomain.com

# Clear your local DNS cache
# macOS:
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
# Linux:
sudo systemd-resolve --flush-caches

Strategy 2: File and Database Rollback

If you have already switched DNS and need to roll back to a previous state on the same host, you need both file and database backups.

Before starting any migration, create dated backups:

# Database backup
wp db export pre-migration-$(date +%Y%m%d).sql --single-transaction

# Files backup
tar czf pre-migration-files-$(date +%Y%m%d).tar.gz wp-content/

To restore:

# Restore database
wp db import pre-migration-20220719.sql

# Restore files
tar xzf pre-migration-files-20220719.tar.gz

Strategy 3: Host-Level Snapshots

Most managed hosts provide backup and restore functionality:

WP Engine: Automatic daily backups plus on-demand backup/restore through the User Portal. You can create a backup point immediately before migration and restore to it with a single click.

Kinsta: Daily automatic backups (retained for 14 to 30 days depending on plan) plus manual backups. Restore is one-click in MyKinsta.

Pantheon: Automatic daily backups plus on-demand backups via Terminus:

terminus backup:create your-site.live
# To restore:
terminus backup:restore your-site.live --element=all

Cloudways: Automated backups on a configurable schedule. Restore through the Cloudways dashboard.

Strategy 4: Parallel Running

For critical sites where downtime is not acceptable, run both hosts in parallel:

1. Set up the new site on a temporary URL and verify it completely.
2. Use a load balancer or Cloudflare Worker to route a small percentage of traffic to the new host (canary deployment).
3. Monitor error rates and performance on both hosts.
4. Gradually increase traffic to the new host.
5. Once 100% of traffic is on the new host and stable, decommission the old host.

This is more complex and more expensive (you are paying for two hosts simultaneously), but it eliminates the risk of a hard cutover.

The 72-Hour Rule

Do not cancel your old hosting account for at least 72 hours after completing the migration, and preferably longer (I recommend two full weeks). DNS propagation can take up to 72 hours in edge cases. Some visitors, especially those behind corporate proxies or in regions with aggressive DNS caching, may continue reaching the old host for days after you change DNS records.

If you cancel the old host immediately and some visitors are still being routed there, those visitors will see an error page. Keep the old host alive, even if it is serving a stale version of the site.

Common Pitfalls and How to Avoid Them

After hundreds of managed host migrations, these are the problems I see most often.

Pitfall 1: Forgetting to Update wp-config.php Constants

Every managed host adds its own constants to wp-config.php. Some of these constants control behavior that the new host handles differently. Common offenders:

define('WP_SITEURL', 'https://old-domain.com');    // Hardcoded URL
define('WP_HOME', 'https://old-domain.com');         // Hardcoded URL
define('UPLOADS', 'wp-content/uploads');              // Custom uploads path
define('WP_CONTENT_DIR', '/old/path/wp-content');     // Old file path
define('WP_CONTENT_URL', 'https://old-cdn.com/wp-content'); // Old CDN URL

Always review the entire wp-config.php file and update or remove host-specific values.

Pitfall 2: Ignoring .htaccess or Nginx Rules

Custom redirect rules, security rules, and performance rules often live in .htaccess (Apache) or Nginx configuration files. Some managed hosts use Apache; others use Nginx. Rules from one web server do not translate directly to the other.

If you are moving from an Apache-based host to an Nginx-based host (or vice versa), you need to convert your rewrite rules. WP Engine uses Nginx. Kinsta uses Nginx. Pantheon uses Nginx. Cloudways offers both.

# Apache .htaccess redirect:
Redirect 301 /old-page /new-page

# Nginx equivalent (goes in nginx.conf or custom rules):
rewrite ^/old-page$ /new-page permanent;

Pitfall 3: Not Testing Contact Forms

Contact forms (Contact Form 7, Gravity Forms, WPForms) often break after migration for one of two reasons:

1. The form sends email using the server's mail function, and the new host restricts outbound email.
2. The form uses AJAX submission, and the AJAX endpoint URL is hardcoded somewhere.

Test every form on the site after migration.

Pitfall 4: Scheduled Content Not Publishing

If wp-cron is disabled and the new host's server-level cron is not configured, scheduled posts will not publish. This is a subtle issue that you might not notice until a scheduled post misses its publish date. Verify cron is working:

wp cron test
# Expected output: "Success: WP-Cron spawning is working correctly."

Pitfall 5: Multisite Complications

WordPress Multisite migrations are significantly more complex because you need to update URLs for every site in the network. The search-replace must cover multiple tables:

wp search-replace 'old-domain.com' 'new-domain.com' --all-tables --network --precise --recurse-objects --url=old-domain.com

Additionally, the wp_blogs and wp_site tables need manual verification:

wp db query "SELECT * FROM wp_blogs"
wp db query "SELECT * FROM wp_site"

Domain mapping, if used, adds another layer of complexity. Test each site in the network individually.

Platform-Specific Quick Reference

Here is a condensed reference for the most common managed host migrations. Use this as a checklist alongside the detailed sections above.

WP Engine Outbound Checklist

[ ] Remove mu-plugin.php and wpengine-common/ from mu-plugins
[ ] Remove force-strong-passwords.php from mu-plugins
[ ] Remove object-cache.php (Memcached drop-in)
[ ] Remove WPE_APIKEY, WPE_CLUSTER_ID, PWP_NAME from wp-config.php
[ ] Remove WP Engine-specific .htaccess rules (if applicable)
[ ] Replace any WpeCommon:: cache purge calls in theme/plugins
[ ] Export database with wp db export --single-transaction
[ ] Download files via SFTP or rsync

Kinsta Outbound Checklist

[ ] Remove kinsta-mu-plugins/ and kinsta-mu-plugins.php from mu-plugins
[ ] Remove object-cache.php (Redis drop-in)
[ ] Remove WP_REDIS_HOST, WP_REDIS_PORT from wp-config.php
[ ] Remove Kinsta CDN references if using their CDN feature
[ ] Replace kinsta_cache purge calls in theme/plugins
[ ] Export database via SSH or phpMyAdmin
[ ] Download files via SFTP

Pantheon Outbound Checklist

[ ] Clone code repository via Git
[ ] Download files (uploads) separately via Terminus rsync
[ ] Export database via Terminus backup
[ ] Remove pantheon-mu-plugin/ and pantheon.php from mu-plugins
[ ] Remove object-cache.php if present
[ ] Replace Pantheon-specific wp-config.php (environment variable-based config)
[ ] Remove PANTHEON_ENVIRONMENT references from theme/plugins
[ ] Account for read-only file system differences

Cloudways Outbound Checklist

[ ] Deactivate and remove Breeze plugin
[ ] Remove Breeze rules from .htaccess
[ ] Remove Cloudways-specific wp-config.php constants
[ ] Remove Redis/Memcached configuration
[ ] Document any custom Varnish (VCL) rules for replication
[ ] Export database with wp db export
[ ] Download files via SFTP or rsync

Automating Migrations with Scripts

If you perform managed host migrations regularly, automating the repetitive parts saves significant time and reduces the chance of human error. Here is a basic migration script framework:

#!/bin/bash
# migration-prep.sh
# Prepares a WordPress site for migration from a managed host

set -e

SITE_DIR="${1:-.}"
BACKUP_DIR="./migration-backup-$(date +%Y%m%d-%H%M%S)"

echo "=== WordPress Migration Preparation ==="
echo "Site directory: $SITE_DIR"
echo "Backup directory: $BACKUP_DIR"

mkdir -p "$BACKUP_DIR"

# Step 1: Database backup
echo "[1/6] Exporting database..."
cd "$SITE_DIR"
wp db export "$BACKUP_DIR/database.sql" --single-transaction
echo "Database exported: $(du -h "$BACKUP_DIR/database.sql" | cut -f1)"

# Step 2: Document mu-plugins
echo "[2/6] Documenting mu-plugins..."
ls -la wp-content/mu-plugins/ > "$BACKUP_DIR/mu-plugins-list.txt" 2>/dev/null || echo "No mu-plugins directory"

# Step 3: Document active plugins and themes
echo "[3/6] Documenting plugins and themes..."
wp plugin list --fields=name,status,version > "$BACKUP_DIR/plugins-list.txt"
wp theme list --fields=name,status,version > "$BACKUP_DIR/themes-list.txt"

# Step 4: Document cron events
echo "[4/6] Documenting cron events..."
wp cron event list --fields=hook,next_run_relative,recurrence > "$BACKUP_DIR/cron-events.txt"

# Step 5: Document wp-config.php constants
echo "[5/6] Documenting wp-config constants..."
wp config list --fields=name,value > "$BACKUP_DIR/config-constants.txt"

# Step 6: Generate uploads manifest
echo "[6/6] Generating uploads manifest..."
find wp-content/uploads/ -type f | wc -l > "$BACKUP_DIR/uploads-count.txt"

echo ""
echo "=== Preparation Complete ==="
echo "Backup saved to: $BACKUP_DIR"
echo "Review the backup directory contents before proceeding with migration."

Final Thoughts on Managed Host Migrations

The migration itself is the easy part. The hard part is the platform-specific knowledge: knowing which files to remove, which configurations to change, and which edge cases to watch for. Every managed WordPress host adds its own layer of abstraction on top of WordPress, and moving between hosts means peeling off one layer and applying another.

The three most important principles for a successful managed host migration are:

1. Audit before you act. Spend the time to fully understand what your current host has installed, configured, and modified. The pre-migration audit is the single most valuable step in the entire process.

2. Use WP-CLI for everything database-related. Manual SQL queries and phpMyAdmin find-and-replace will corrupt serialized data. WP-CLI's search-replace command exists specifically to handle WordPress's data format correctly. Use it.

3. Keep the old host running. Your rollback plan is only as good as your ability to execute it. If the old host is still running with the original site intact, you can always switch DNS back and buy yourself time to troubleshoot. Once you cancel that old account, your safety net is gone.

Managed host migrations take longer than standard migrations. Budget twice the time you think you need, test on a staging environment before touching production, and always have a rollback plan. The extra preparation pays for itself the moment something unexpected happens, and in my experience, something unexpected always happens.

This guide covers the most common migration paths I encounter in my work. Each migration will have its own unique complications depending on the specific plugins, theme customizations, and infrastructure configuration involved. Use this as a foundation, adapt it to your specific situation, and do not hesitate to reach out to the support teams at both your old and new hosts. They have seen these migrations before and can often provide platform-specific guidance that no general guide can cover.

Share this article

Sarah Kim

Systems administrator and WordPress hosting specialist. Has managed infrastructure at two managed WordPress hosting companies. Writes about server stacks, caching, and monitoring.