The real risk: two databases that drift apart
A WordPress site is made of files, which you can copy safely, and a database, which changes with every visit: order, sign-up, comment, form entry, cart. As soon as you export the database, the old one keeps writing and your copy goes stale. As long as DNS points to the old server, the gap is invisible. On cutover day it becomes a loss: everything that arrived after the export exists only on the old server.
The method fits in one sentence: build and test the copy in advance, then freeze writes, take a last pass of files and database, and only then change DNS. The general order of a hosting change (TTL, e-mail, domain) is covered in another guide; this one deals with what is specific to WordPress.
D-7: inventory the site
Note the PHP version, the plugins that write (shop, forms, bookings, subscriptions), the scheduled tasks and the payment gateways with their callback addresses.
wp core versionwp plugin list --status=activewp cron event list
Also note where e-mail lives: it will not move.D-3: lower the TTL
Set the TTL of the site's A and AAAA records to 300 seconds, at least one old TTL period before the cutover. Do not touch MX or TXT records.
D-2: first full copy
Files:
rsync -az --info=progress2 user@old-server:/path/to/site/ /path/to/site/
Database:wp db export site.sql --single-transactionon the old server, thenwp db import site.sqlon the new one. Time both: that duration sizes the freeze window.D-2: isolate the copy
On the copy, add
define('DISABLE_WP_CRON', true);towp-config.phpand switch off the shop's e-mails. The copy holds real customers: a replayed scheduled event would send duplicate messages.D-1: test through the hosts file
Browse the copy under its real name without touching public DNS (dedicated section below). Note every anomaly, fix it, start over.
D-day, quiet hour: freeze and take the last difference
Put the source in maintenance with
wp maintenance-mode activate, redo the database export, resynchronise files with the samersynccommand, import, then retest through hosts.D-day: switch DNS and issue the certificate
Replace the A and AAAA records with the new server's addresses, leaving MX alone. Issue the certificate as soon as the name points at the destination, then remove
DISABLE_WP_CRON.D-day to D+14: verify, keep, cut
Check the old server's logs and the checklist below. Keep the old server in maintenance, untouched, for at least two weeks before cancelling it.
Copy files and database, then set up wp-config.php
Copy files with rsync rather than an archive uploaded by hand: it keeps permissions, resumes after an interruption and only retransfers what changed, which makes the last pass very short. For the database, prefer wp db export or mysqldump --single-transaction, which produces a consistent export without locking InnoDB tables.
On the new server, wp-config.php is the first file to reread. DB_NAME, DB_USER and DB_PASSWORD must match the database created on the destination. DB_HOST is often localhost, but some hosts require a separate host, a port (host:3307) or a socket. Also check $table_prefix, which must match the imported tables, otherwise WordPress thinks the installation is empty. Finally, WP_HOME and WP_SITEURL, if defined in the file, override the database: an old value left here sends every visitor back to the old address.
Replace URLs in the database without breaking serialized data
WordPress stores absolute addresses in posts, options, menus and theme and plugin settings, often inside serialized values whose length is written into the data. A sed on the export changes the string length without fixing that counter, and the value becomes unreadable: empty widgets, reset theme. wp search-replace reads and rewrites these structures correctly.
Replacement is only needed if the address changes: new domain name, a move from http to https, or a copy built under a temporary address. If you test through the hosts file under the real name, there is nothing to replace. Always start with wp search-replace 'http://old.your-domain.com' 'https://your-domain.com' --all-tables --skip-columns=guid --dry-run, which counts replacements without writing anything. If the number surprises you, stop. Then rerun without --dry-run, on the copy and never on the source, followed by wp cache flush. The --skip-columns=guid option leaves post identifiers untouched, which RSS readers use to recognise content they have already seen.
Test the copy before DNS, through the hosts file
The hosts file on your computer tells your machine alone where to send a domain name, without touching public DNS. Add a line 203.0.113.10 your-domain.com www.your-domain.com (the new server's address) to /etc/hosts, or to C:\Windows\System32\drivers\etc\hosts on Windows. For a single request, curl --resolve your-domain.com:443:203.0.113.10 -I https://your-domain.com/ does the same without changing the machine. Clear the browser cache, which keeps the previous resolution.
Test journeys, not the home page: admin login, a deep page (which reveals permalinks), a media upload, search and, for a shop, a cart through to the payment page. The copy's certificate may not be issued yet (see below): a warning is then expected, a blank page or a PHP error is not. Remove the hosts line afterwards: a forgotten entry makes you believe for weeks that the cutover has not happened.
Freeze writes for sites with orders and forms
A brochure site can switch without freezing anything: the possible loss comes down to a comment. As soon as the site takes payments or collects requests, you need a window without writes. The sequence: announce a short maintenance, put the source in maintenance, redo the database export, resynchronise files (rsync now only transfers the difference, often just the new media), import on the destination, replay the search-replace if there is one, test through hosts, then change DNS. The old server stays in maintenance until the end.
This window lasts a few minutes for a database of ordinary size, and it is a duration to measure during the first copy. Payment gateways are a special case: they send notifications to the configured address and replay failed ones under their own retry policy, to be confirmed in their documentation. Check in their dashboard that callbacks refused during maintenance are replayed once the new site is live.
What writes during the window, and how to stop it
- WooCommerce orders — the source's maintenance mode blocks them; a customer with a cart already open finds an unavailable site, hence the prior announcement.
- Contact and quote forms — depending on the plugin, entries live in the database, in a file or are only sent by e-mail; find where each one writes, because an entry stored on the old server after the export is lost.
- Comments and sign-ups — close them in the discussion and registration settings for the length of the window if maintenance mode is not possible.
- Scheduled tasks —
wp cron event listlists them; the copy must not run the same ones as the source before the cutover, otherwise reminders and renewal e-mails go out twice. - Incoming webhooks — payment gateways, CRM, shipping tool: their address points at a domain name, so at the new server after the cutover; check that signing secrets were copied with the settings.
- Visitors still reaching the source — a long-cache resolver sends them there after the cutover; the old server can relay them to the new one while keeping the
Hostheader, a step reserved for teams comfortable with reverse proxies.
Post-cutover symptoms and their cause
Scroll the table
| Symptom | Likely cause | Fix |
|---|---|---|
| Redirect loop (`ERR_TOO_MANY_REDIRECTS`) | WordPress forces `https` but only sees `http`: a proxy, CDN or load balancer terminates TLS, or the CDN's SSL mode is Flexible | Full (strict) mode on the CDN, or read `X-Forwarded-Proto` in `wp-config.php` to set `$_SERVER['HTTPS']` |
| Error establishing a database connection | Wrong `DB_HOST`, credentials or user privileges; MySQL server stopped | `wp db check`, then a manual test with `mysql -h host -u user -p`; grant privileges on the database |
| Empty installation or install screen | `$table_prefix` differs from the real table prefix | Align the prefix with the output of `SHOW TABLES` |
| Mixed content: broken padlock, blocked images | `http://` addresses left in posts or options | `wp search-replace` from `http://` to `https://`, after a `--dry-run` |
| Posts return 404, home page works | Rewrite rules missing: `.htaccess` not copied or not read, or nginx without `try_files` | `wp rewrite flush --hard`; on nginx, `try_files $uri $uri/ /index.php?$args;` |
| Menus, widgets or theme reset | Serialized values corrupted by a blind replacement | Reimport the export, then redo the replacement with `wp search-replace` |
| Old addresses still in pages | `WP_HOME` or `WP_SITEURL` hard-coded, or page cache not purged | Fix the constants, `wp cache flush`, purge the page cache |
E-mail and certificate: two topics that do not follow the site
Web hosting and e-mail are two separate services, carried by different DNS records: MX for receiving, TXT (SPF, DKIM, DMARC) for sending. A web migration must change neither the MX records nor the mailboxes. Check it before and after with dig MX your-domain.com +short and dig TXT your-domain.com +short: both outputs must be identical. The trap is sending: if the SPF lists the old server's address (ip4:) and WordPress sends its e-mails from the new one, order notifications end up in spam. Add the new address to the SPF or, better, route WordPress through an authenticated SMTP.
The certificate follows the address. With HTTP validation (certbot --nginx or cPanel AutoSSL), the certificate authority must reach the new server under the real name, which is only possible after the cutover: issue it right away and expect a warning before then, including during your hosts-file tests. If that delay is unacceptable, DNS validation lets you issue the certificate before the cutover, at the cost of API access to the zone. Then check the expiry with curl -vI https://your-domain.com/ 2>&1 | grep -i expire.
What to check before cutting the old server
- Traffic really has moved — follow the old server's access logs with
tail -f: only bots and long-cache resolvers should appear, then nobody. - Writes land in the right place — place an order with the gateway in test mode, submit a form, and find both in the database and by e-mail.
- Volumes are comparable — compare row counts of key tables (
wp db query "SELECT COUNT(*) FROM wp_posts", adapt the prefix) and the size ofwp-content/uploadsagainst the source. - Scheduled tasks run —
wp cron event run --due-nowmust run without errors; wire up a real system cron if traffic is low. - Mail is intact — MX, SPF and DKIM unchanged, and a test message received in a real mailbox.
- A final backup exists — database export and copy of the old server's files, archived off that server, before any shutdown.
Decide the rollback before the cutover
Rollback is simple as long as the old server stays intact: put the old address back in the A and AAAA records, using the low TTL already set. It stops being simple once the new database has received orders, because going back erases them. So fix the rule before D-day: rollback is possible without condition until the first real write on the new site, and you fix forward beyond that. Keep the old server intact and paid for at least two weeks: that is the price of reversibility.
Across a client portfolio, the method becomes a routine
Write this sequence once, with the measured durations, and reuse it from one client to the next: inventory sheet, freeze window, checklist. The client does not need to understand DNS; they need to know when the maintenance will happen and who confirms that everything works. On ServOrbit, reseller hosting groups your clients' accounts, and assisted migration is an option for the sites you would rather not move yourself. Domains, hosting plans and renewals are tracked from one space, under your brand.