·SafeGuard Team

3 Ways to Migrate a WordPress Site (Without Losing Data)

Direct site-to-site, portable installer, and cloud relay. We explain how each works, the failure modes that have nothing to do with your hosting, and how SafeGuard handles them.

The migration is supposed to be the easy part. You backup site A, you restore on site B, you flip DNS, done. Then you load site B and the homepage is half white, the menu is gone, and Elementor says "There has been a critical error."

Migration is not the easy part.

Most of the failures aren't about hosting. They're about the way WordPress stores data. Once you know what's actually fragile, the choice of migration method becomes obvious. We'll explain the failure modes first, then walk through the three ways SafeGuard moves a site, and which to pick when.

Why "search and replace the URL" fails

WordPress doesn't just store URLs in wp_posts.post_content. It stores them in serialized PHP arrays scattered across wp_options, wp_postmeta, wp_termmeta, and any custom tables a plugin uses. Those serialized values include byte-length prefixes:

s:23:"https://oldsite.com/foo"

The 23 is the length of the string in bytes. If you replace oldsite.com (11 chars) with newsite.io (10 chars) using a plain sed or wp search-replace --regex, the string gets one byte shorter but the 23 stays. PHP's unserialize() then bails out because the length is wrong, and the option silently becomes false. The "critical error" you see in WordPress is sometimes literally one widget with a corrupted serialized blob.

There are three places this matters in practice:

  • Theme settings. Customizer settings, widget configurations, menu data, all serialized.
  • Page builder content. Elementor stores section trees as nested serialized arrays. So does Beaver Builder. So do most form builders.
  • Plugin meta. WooCommerce stores order line items, product attributes, and shipping addresses as serialized arrays. Yoast SEO stores site-wide settings. WPBakery uses serialized shortcode metadata.

A migration tool either handles serialization correctly or it doesn't. There's no middle ground. SafeGuard's approach is to actually unserialize() the value, walk the resulting tree (recursing into nested serialized strings, which Elementor uses), do the string replacement on the leaves, then serialize() it back. If unserialize fails for any reason (usually a value containing weird characters that broke serialization at write time, not by us), there's a regex-based length-fixer fallback. We log when the fallback fires so we can investigate.

The other failure mode worth knowing about: character set mismatches. Old WordPress sites are often utf8 (3-byte). New WordPress is utf8mb4 (4-byte, supports emoji and most Asian scripts). Moving from one to the other without converting truncates 4-byte characters mid-byte. SafeGuard detects mismatches at preflight and converts. Most plugins don't.

Use when: both sites can reach each other over the internet.

SafeLink is SafeGuard's site-to-site protocol. Both sides need the plugin installed. The destination site generates a connection code; you paste it into the source site; the source pushes the backup chunks straight to the destination's REST endpoint.

Things SafeLink does that matter:

  • HMAC-signed requests. Every chunk has a signature derived from a per-session key. The destination rejects anything not signed by the source it agreed to talk to.
  • AIMD adaptive concurrency. This is the part most people don't know they want. The uploader starts at 1 parallel connection. If a chunk's response time stays low, it adds another connection. If the destination starts taking longer (signal that you're hitting its limits), it backs off. The ceiling is 8 parallel connections. On a fast destination you get full bandwidth; on a tired shared host you don't crater it.
  • Resumable. Each chunk is a separate request. If the connection dies in the middle of an 8 GB site, the next attempt picks up at the last completed chunk. You don't restart the whole transfer.
  • Preflight checks. Before the transfer starts, the source asks the destination for its WordPress version, PHP version, disk space, and installed plugins. If the destination is on PHP 7.4 and the source uses PHP 8.1 syntax in a custom plugin, you get a warning before you commit. If the destination has 600 MB free and the migration needs 2 GB, you get a warning.

In practice you go to SafeGuard → Migrate on the destination, click "Generate Connection Code", paste the code on the source, click "Migrate". The whole thing is one screen on each side.

When it doesn't work: if the destination is behind a firewall that blocks incoming POSTs (corporate network, some Cloudflare configurations, NAT without port forwarding). For those cases you want method 3.

Method 2: Portable installer

Use when: the destination is a blank server. No WordPress yet, just FTP or a file manager.

This is the case most plugins handle badly. You have a fresh cPanel account, a blank database, and a domain pointed at it. You don't have WP-CLI. You don't have SSH. You can drag files into the file manager.

The portable installer is one PHP file plus the backup archive. You drop both at the document root, hit https://yourdomain.com/safeguard-installer.php in a browser, and a wizard takes over. It:

  1. Asks for the new database credentials (host, name, user, password).
  2. Validates the credentials by actually connecting and creating a temp table.
  3. Extracts the archive.
  4. Imports the SQL dump.
  5. Walks all the tables that contain serialized data and rewrites URLs/paths using the same unserialize-walk-reserialize logic as SafeLink.
  6. Writes a new wp-config.php with the right credentials and salts.
  7. Sets sane file permissions (644/755).
  8. Deletes itself and the archive on success, so you don't leave a self-running installer at a public URL.

Step 8 matters. We've seen sites where the migration succeeded but the leftover installer file was indexed by Google and used by an attacker to take over the site weeks later. Cleanup isn't optional.

The portable installer is what you reach for when:

  • You're moving from a managed host that doesn't allow plugin uploads (some "WordPress hosting" tiers lock the file system).
  • The destination is a fresh DigitalOcean droplet with just LAMP installed.
  • You're handing the migration off to a client who can FTP but not much else.

Method 3: Cloud relay

Use when: direct connection isn't possible, and you don't want to mess with FTP.

Some destinations can't accept incoming traffic at all. Cloudflare-only access, behind a VPN, NAT without port forwarding, or corporate networks that block everything except port 443 outbound. SafeLink fails immediately on these because the destination can't be reached.

Cloud relay works around it by routing both sides through SafeGuard's relay server. The source encrypts each chunk with a session key the destination knows, uploads to the relay, and the destination polls the relay for the next chunk. Both sides only need outbound HTTPS.

The relay never has the decryption key. Files are encrypted before upload using authenticated encryption (AES-GCM); the relay sees ciphertext and a session ID, nothing else. After the destination confirms successful extraction, the relay deletes the chunks. There's no archive sitting on our infrastructure waiting to be exfiltrated.

Cloud relay is slower than direct transfer because it's two hops instead of one. The tradeoff is it works in environments where direct transfer can't even start. SafeGuard auto-detects when SafeLink fails (e.g. timeout on the initial handshake) and offers to switch to relay. You can also force relay mode from the start if you know the destination is firewalled.

Comparison

Direct (SafeLink)Portable InstallerCloud Relay
Plugin on destinationRequiredNot neededRequired
WordPress on destinationRequiredNot neededRequired
Works behind firewallNoN/A (manual)Yes
Encrypted in transitYesN/AYes (E2E)
ResumableYesYesYes
SpeedFastestMediumSlowest
Best forStandard movesBlank serversRestricted networks

Things that go wrong, and how SafeGuard handles them

Serialized data corruption. Covered above. Unserialize, walk, reserialize. Fallback to regex length-fix only if unserialize fails outright.

Charset mismatches. Detected at preflight. SafeGuard converts utf8utf8mb4 during import if needed. The conversion is one-way; you can't go back to utf8 without losing 4-byte characters anyway.

Files outside wp-content. Some plugins drop files at the WordPress root or in mu-plugins. Most backup tools only archive wp-content and skip the rest. SafeGuard archives wp-content, wp-config.php, mu-plugins, and any custom directories you've added at the root, then restores them in the right place.

Permission issues. New host has different www-data UID. SafeGuard re-applies sane permissions (644 for files, 755 for directories) at restore time. If it can't (rare on managed hosts), it surfaces a warning instead of silently leaving the site unwritable.

Large sites timing out. The chunked, resumable transfer means a 30 GB media-heavy site moves in pieces, and any single failed chunk just gets retried. There's no "it failed at 87% so we restart" situation.

Multisite. Subsite extraction is supported via Subsite_Extractor. You can pull a single subsite out of a network and move it to a standalone install, which is the case where most other tools just give up.

We have 188 end-to-end tests around migration that run on every commit, exercising all three methods across WordPress versions, PHP versions, character sets, and a handful of nightmare site configurations (heavy Elementor, WooCommerce with 50K orders, multisite networks, sites with custom upload paths). When they pass, that's the green light to ship.

Try it

Pick the method that matches your situation. Direct for standard moves, portable for blank servers, relay for restricted networks. SafeGuard auto-detects which one your migration needs and falls back automatically when the first choice fails.

Get Early Access →