wordpress
Migrating a 50GB WooCommerce site with zero downtime — the runbook we actually use
Big WooCommerce migrations fail in predictable ways. Here's the runbook we follow, the gotchas to plan for, and the cutover script that ties it together.
22. Mai 2026 · 10 min · von Sudhanshu K.
Migrating a 50GB WooCommerce site with zero downtime — the runbook we actually use
Last month I moved a WooCommerce site with 50GB of data, 480,000 products, and 14 years of order history from a creaking single-server VPS onto a properly-architected DigitalOcean setup. Total customer-facing downtime: 38 seconds, during which we held cart sessions open with a 503-with-Retry-After.
This is the runbook we used. It's the same one we run for every WooCommerce migration we take on — adapted to the specifics, but the bones are unchanged.
Why WooCommerce migrations fail
Before the runbook, the failure modes. Most WooCommerce migrations go wrong in one of five ways:
- Serialized data corruption — WordPress stores settings as serialized PHP arrays, and naive find-replace breaks the length prefixes
- The mysqldump-and-import marathon — a single mysqldump of a 50GB database takes hours; the import takes more hours; orders placed during that window vanish
- Session and cart state — WooCommerce stores in-progress carts in the database; if you cutover mid-checkout, customers lose their carts
- CDN and DNS propagation — the new site goes live, but Cloudflare is still pointing the cache at the old origin, or 30% of users are still resolving DNS to the old IP for the next 24 hours
- Payment gateway webhooks — Stripe/PayPal are sending order-completion webhooks to the old origin, and orders silently fail to update post-cutover
Every step below addresses one of these explicitly.
Step 0: Snapshot the source
Before touching anything, take a complete snapshot of the source server. Volume snapshot, filesystem-level. This is your "undo button" if anything goes sideways. We've never had to use it. We've also never not taken it.
Step 1: Provision the destination
On the new host, install the WordPress stack: Nginx + PHP-FPM + MySQL/MariaDB + Redis, matching the source PHP and MySQL versions exactly. (Mismatched versions = migration day is upgrade day, which is a separate problem you don't want to compound.)
A critical detail: provision the database with the same character set and collation as the source. We've seen migrations from utf8 to utf8mb4 quietly corrupt product names with emoji in them.
mysql -e "CREATE DATABASE wp_new \
CHARACTER SET utf8mb4 \
COLLATE utf8mb4_unicode_520_ci;"If you're moving across cloud providers as well as servers, this is also where you make the "where will we live in 2027" decision. We're cloud-agnostic — we run customer sites on AWS, GCP, Azure, and DigitalOcean depending on what the workload needs. For high-volume WooCommerce specifically, DO's predictable bandwidth pricing usually wins.
Step 2: Bulk-copy the heavy stuff before cutover
This is the trick that turns "8 hours of downtime" into "38 seconds." You copy the bulk of the data while the source is still live and taking orders. Then on cutover day, you only need to replicate the delta.
Files: rsync with bandwidth limits
rsync -avz --bwlimit=20000 --delete \
--exclude='wp-content/cache' \
--exclude='wp-content/uploads/wc-logs' \
source.example.com:/var/www/html/ \
/var/www/html/Run this in a screen/tmux session a week before cutover. It might take 6 hours the first time. Then re-run it every night until cutover — each subsequent run is small because rsync only transfers the diff. By cutover day, the final rsync is a couple of minutes.
Database: set up replication
This is the part most migration tutorials skip. Don't mysqldump. Set up MySQL replication from source to destination:
-- On source
GRANT REPLICATION SLAVE ON *.* TO 'replica'@'%' IDENTIFIED BY 'redacted';
FLUSH PRIVILEGES;
SHOW MASTER STATUS; -- note the File and Position
-- On destination
CHANGE MASTER TO
MASTER_HOST='source.example.com',
MASTER_USER='replica',
MASTER_PASSWORD='redacted',
MASTER_LOG_FILE='mysql-bin.000142',
MASTER_LOG_POS=998877;
START SLAVE;(Or use GTID-based replication if both servers support it — it's cleaner for the cutover.)
Now the destination's database is staying in sync with the source automatically. By the time you cut over, the destination has every order, every cart, every inventory tick — up to the second.
If your hosting provider doesn't let you set up cross-host replication (some don't), you can fall back to logical replication with Percona's pt-online-schema-change family of tools, or use AWS DMS / Cloud SQL replication if you're crossing into a managed database. We can help architect this as part of a provisioning engagement — it's not always obvious which approach a given source environment supports.
Step 3: Verify the destination, dry-run the cutover
A week before cutover, you should have a fully-functional copy of the production site running at a staging.example.com subdomain on the new infrastructure. Run a full dry-run cutover against it:
- Browse the catalog
- Add to cart
- Check out as a guest
- Check out as a logged-in customer
- Apply a coupon
- Process a refund (via admin)
- Verify payment gateway sandbox webhooks land correctly
- Run
wp wc tool run regenerate_thumbnailsand confirm no errors - Run
wp doctor check --alland resolve any warnings
If anything fails on the dry-run, fix it now — not at 2am on cutover night.
Step 4: The cutover script
Cutover happens in a tight sequence. We put it in a script so it's not happening from manual SSH at 3am:
#!/bin/bash
set -e
T=$(date +%s)
# 1. Put the source into read-only / maintenance mode
ssh source.example.com "wp maintenance-mode activate --path=/var/www/html"
# 2. Wait for in-flight checkout to drain (60s)
sleep 60
# 3. Final database sync — wait for replication lag = 0
ssh dest.example.com "mysql -e 'SELECT MASTER_POS_WAIT(\"mysql-bin.000142\", $POS);'"
# 4. Promote destination DB to master
ssh dest.example.com "mysql -e 'STOP SLAVE; RESET SLAVE ALL;'"
# 5. Final rsync of files (in case of late-write to uploads)
rsync -avz --delete source.example.com:/var/www/html/wp-content/uploads/ \
/var/www/html/wp-content/uploads/
# 6. wp-cli search-replace for any hardcoded URLs (handles serialized data)
ssh dest.example.com "wp search-replace 'https://source.example.com' 'https://example.com' --all-tables --skip-columns=guid"
# 7. Flip DNS at Cloudflare via API
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records/$RECORD" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
--data '{"content":"203.0.113.42"}'
# 8. Purge Cloudflare cache
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
-H "Authorization: Bearer $CF_TOKEN" \
--data '{"purge_everything":true}'
# 9. Deactivate maintenance mode
ssh dest.example.com "wp maintenance-mode deactivate --path=/var/www/html"
echo "Cutover complete in $(( $(date +%s) - T )) seconds"The actual customer-facing window — from "site goes into maintenance" to "site responds from new origin" — is steps 2-9. With DNS pre-lowered to 60s TTL the week before, and Cloudflare proxying so the IP change is at edge level, end users don't even notice DNS.
Step 5: The 72-hour watch
After cutover, you watch:
- Order count per hour, compared to the previous week at the same hour — should match
- Payment gateway webhook log — every webhook should be 2xx
- Error log — any PHP fatals get paged
- Replication lag on the reverse direction (we leave the old source as a replica of the new master for 72 hours, as a "hot rollback" path)
- Cloudflare analytics for any spike in 5xx
Anything anomalous, you can rollback DNS in 60 seconds — because the old infrastructure is still there, still in-sync as a replica, ready to take traffic.
Three days post-cutover, you decommission the old box.
What this costs in time
For the 50GB site I mentioned at the top:
- Planning + dry run: 2 days
- Pre-cutover incremental copies running in background: 5 days elapsed, ~6 hours of cumulative engineer time
- Cutover night: 90 minutes (most of which is watching things and verifying)
- Post-cutover monitoring: 30 minutes a day for 3 days
Total customer downtime: 38 seconds.
The alternative — a "let's just stop the world for 8 hours" migration — costs the customer their busiest 8 hours, plus the panic of every order placed during cleanup. WooCommerce shops at any meaningful scale can't afford it. The replication-based approach is genuinely not much harder once you've done it twice; it just takes the right tooling and the right runbook.
If you're staring down a migration that fits this shape, give us a shout — we do these regularly enough that we've automated most of the runbook.
Sudhanshu K. is Principal Engineer at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). He has migrated more WooCommerce shops than he can count, including several with seven-figure GMV through a single weekend cutover.