Moving WordPress to a New Host: What Breaks Quietly After It Succeeds

No Comments

Photo of author

By Liu Yu

Every guide to moving WordPress to a new host ends at the same place: the site loads on the new server, so the migration worked. That is where ours starts, because in our experience the migration itself is rarely the hard part. Plugins copy files and databases reliably enough. What goes wrong is quieter — things that keep returning HTTP 200 while being broken, unpaid for, or publicly readable.

We move sites for a living, mostly export-facing WordPress installs. The list below is drawn from a batch of roughly forty migrations off one shared platform onto self-managed servers, plus the individual moves before and after it. Every item here is something we found after a migration was already considered finished.

1. The migration that never actually happened

The most expensive failure is also the least visible: the files and database moved, and DNS never followed.

Nothing looks wrong. The new server serves the site correctly if you visit it by IP or a temporary hostname. The old host keeps serving real traffic. Both copies return 200. Months pass.

We audited a group of sites long after their migration was marked complete and found nine of them still resolving to the old provider’s IP. The copies on the new servers had been sitting there consuming disk and PHP workers with zero traffic, while the old hosting account kept billing.

What made it obvious once we looked at the right thing:

# Where does the world actually reach this site?
dig +short A example.com
dig +short A www.example.com
dig +short AAAA example.com
dig +short AAAA www.example.com
dig +short CNAME www.example.com

Compare the answers with the new server’s address. A mismatch is not automatically a failure — if you sit behind a CDN, a reverse proxy or a load balancer, the public record is supposed to point at the edge rather than at your origin. In that case the question moves one layer down: does the CDN’s origin setting point at the new server? Check that in the CDN console, and confirm with the access logs on both machines.

Check AAAA as well as A. A forgotten IPv6 record pointing at the old host will serve a share of your visitors from the old server while the IPv4 record looks perfectly correct.

The decisive evidence is not DNS at all — it is which machine is receiving requests. Look at the access log on the new server and confirm real visitor traffic is arriving, and look at the old one to confirm it has stopped.

A second check that costs nothing and settles it in seconds:

# Newest uploaded file on the new server
find wp-content/uploads -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -3

On the nine sites above, the newest uploads were all from around the migration date and nothing after — copies frozen since the day they were made, which is what pointed us at the DNS in the first place.

Treat that as a lead rather than as proof. Plenty of legitimate sites go months without an upload: brochure sites, anything with media on external storage, anything whose editor has simply not posted. A frozen uploads directory is a reason to check the access log; the access log is the evidence.

Do this check on a fixed schedule after every move — a week later, then a month later. The failure mode here is not a bug, it is an unfinished task that nobody is looking at.

2. The backup archive you left in the web root

Migration plugins build a single archive containing everything, and by default they write it inside wp-content/. That is inside the public web root.

The plugins know this is dangerous, so they drop an .htaccess with deny rules and an empty index.php into the backup folder. On Apache that is usually enough. On OpenLiteSpeed it is not. OpenLiteSpeed does not honour those directives the way Apache does, and we confirmed the consequence from a public connection rather than assuming it:

GET /wp-content/<plugin-backup-dir>/<archive-name>
→ HTTP 200
→ 47,355,082 bytes transferred before we aborted the download
→ first bytes matched the archive format's signature, not an error page

Three other sites on the same server answered ranged requests with HTTP 206, meaning the download is resumable.

Worth being precise about what that archive holds, because it is not just content: the database dump, wp-config.php with the database credentials, administrator password hashes, stored SMTP credentials, and every customer enquiry ever submitted through a form.

One honest mitigation, stated because overstating a finding is its own kind of error: directory listing returned 200 but empty, so an attacker cannot enumerate filenames and would have to guess the archive name, which includes a timestamp. That is a real obstacle. It is not a control you should rely on.

Check every site you have ever migrated:

find /path/to/sites -path '*/wp-content/*' \
  \( -name '*.wpress' -o -name '*.sql*' -o -name '*.tar.gz' -o -name '*.zip' \) -ls

Then verify from outside the server, not from a shell on it — the whole point is what the public can reach. If it downloads, treat the credentials in that archive as disclosed: rotate the database password, the admin passwords and the SMTP credentials. Deleting the file afterwards does not undo the exposure window.

The durable fix is a server-level rule rather than a per-plugin one, since the next plugin will make the same assumption. Block the archive extensions in the server configuration and include it in every virtual host.

3. The site loads, but PHP moved a major version

New hosts default to current PHP. Old sites frequently do not survive the jump, and the symptom is often a blank page rather than an error message.

What breaks is rarely the theme you are looking at. It is an abandoned plugin that has not been updated in four years, calling a function removed in PHP 8. Long-tolerated warnings become fatal errors.

The pragmatic sequence: set the new host back to the version the site was running before, confirm the site returns, and only then upgrade deliberately with the debug log open. Doing it in that order separates “the migration broke it” from “the PHP upgrade broke it”, which are two different investigations that people routinely conflate into one bad afternoon.

If the result is a blank page rather than an error, the cause may not be PHP at all — we work through the four different mechanisms behind a blank WordPress page separately, and one of them is specific to migrations.

4. Mail stopped working and nobody noticed for weeks

Shared hosting almost always has a working local mail transfer agent. A fresh VPS image frequently does not. Nothing in WordPress tells you about the difference — wp_mail() returns false and the request moves on.

The consequence is invisible in exactly the way that hurts most: contact form notifications stop arriving, so it looks like enquiries dried up rather than like a broken server. Password resets stop working, which nobody discovers until someone is locked out. Order emails silently vanish.

One command on the new server settles whether the machine can send at all:

php -r 'var_dump( mail( "you@example.com", "post-migration test", "body" ) );'
which sendmail

bool(false) or no sendmail binary means the machine has no local mail transfer agent. That does not doom every fix — an SMTP or API-based plugin bypasses mail() and the local MTA entirely, which is exactly why it is a valid remedy. What it does mean is that anything relying on PHP’s mail() will keep failing until you either install an MTA or route around it deliberately.

One caveat on the test itself: PHP on the command line can use a different configuration from PHP under the web server. If the CLI check disagrees with what your forms do, trust the web-request behaviour. We cover the four layers this can fail at, and the two ways to fix the transport, in WordPress not sending emails.

Add one thing to your migration checklist that most people skip: submit the real contact form, from a browser, and confirm the message arrives in the real inbox. The plugin’s “send test email” button bypasses the form entirely and will pass on a site whose form is broken.

5. Cache drop-ins pointing at a server that no longer exists

Three files sit directly in wp-content/, load before almost everything, and never appear on the plugins screen: object-cache.php, advanced-cache.php and db.php.

An object-cache.php configured for a Redis instance the old host provided will take the new site down before WordPress finishes loading, and because it is not a plugin, deactivating plugins does not help and recovery mode does not pause it. The same blind spot covers mu-plugins/, which loads unconditionally on every request.

After any migration, look in those two places before anything else:

ls -la wp-content/object-cache.php wp-content/advanced-cache.php wp-content/db.php 2>/dev/null
ls -la wp-content/mu-plugins/

Rename anything that references a service the new server does not run. These files are the single most common reason a migrated site is blank while the database and files are provably intact.

6. The rewrite rule you wrote is not being read

This one costs hours because the evidence contradicts itself: the file on disk is correct, and the behaviour has not changed.

If the new server runs OpenLiteSpeed, it does not re-read .htaccess per request the way Apache does. You can write a perfectly valid rule, verify the file content, reload the page, and see nothing — until the server is restarted. LiteSpeed Enterprise is built for Apache compatibility and behaves much closer to Apache here, so advice written for one does not transfer to the other.

Before you conclude your redirect syntax is wrong, confirm which server you are actually on. Migrating from Apache to OpenLiteSpeed while carrying over Apache habits produces a specific kind of wasted afternoon.

7. It returns 200, but the feature is dead

Status codes are a poor proxy for “working”, and post-migration is where that gap does the most damage. A page can render its header, footer and layout perfectly while the thing the page exists for is broken.

Product filters that return no products. Search that returns nothing. A form that shows a success message but submits nowhere. Pagination that 404s past page one. All of these return 200.

Test the function, not the response. Instead of checking that a category page loads, count the products in the response:

curl -s "https://example.com/product-category/widgets/" \
  | grep -o "product-item-class" | wc -l

Note the grep -o … | wc -l rather than grep -c: the latter counts matching lines, and minified HTML puts an entire product listing on one line.

Pick two or three assertions per site that would fail loudly if the migration broke something structural — a product count, a form submission that lands in the inbox, one page from deep in the pagination — and check those rather than the homepage. The homepage is the least informative page on the site for this purpose, because it is usually the most static.

The same trap shows up in performance work, where every standard check can pass while the site stays slow — we work through what to look at once the checklist comes back green separately.

A post-migration checklist that assumes nothing

Ordered by how quietly each one fails, not by how likely it is:

  1. DNS actually points at the new serverdig +short A on both apex and www, compared against the new IP. Re-check a week later.
  2. No archive left in the web root — search for archive extensions under wp-content/, then try to download one from a public connection. Rotate credentials if it succeeds.
  3. Cache drop-ins and mu-plugins reviewed — anything pointing at old infrastructure gets renamed.
  4. The server can send mail — verified with a real form submission reaching a real inbox, not with a plugin’s test button.
  5. PHP version matched first, upgraded second — as two separate steps with the debug log on.
  6. Function-level assertions pass — product counts, form delivery, deep pagination. Not the homepage.
  7. The old host is decommissioned deliberately — only after the checks above, and only once you have confirmed traffic is arriving at the new server rather than assuming it.
  8. Uploads keep growing — check a month later. A frozen uploads directory on a supposedly live site means traffic is going somewhere else.

Item seven deserves emphasis in the other direction. Cancelling the old hosting the moment the new server responds is how the nine sites in the first section would have gone from “billed twice” to “offline”, because the old server was the one actually serving visitors. Keep the old host running until DNS is confirmed and traffic is visibly arriving at the new one.

We handle migrations and the cleanup afterwards as part of ongoing WordPress support, usually for export businesses whose sites need to stay reachable from both sides of the Great Firewall. If a site was moved recently and you want the checks above run against it, send us the URL.

Leave a Comment