Site-to-site migration
Copy one WordPress site onto another — database, uploads, themes, plugins, the lot. The target keeps its own address. The source is never touched.
What a migration actually does
A migration is a one-shot copy in one direction. The source site takes a fresh full backup of itself, pushes that archive to the target site over the target's own ops-token channel, and the target restores it on arrival. When it finishes, the target is running the source's content on the target's own domain.
It is a replacement, not a merge. The target's database and wp-content are overwritten. There is no partial mode, no “posts only” migration, and no way to preview the result first — see There is no dry run below.
One direction, one shot
Source pushes to target. The target is overwritten; the source is untouched. Run it again whenever you need another copy.
The target keeps its URL
Content moves, the address does not. The engine writes the target's own siteurl and home after the import, then rewrites source URLs to target URLs across the database.
Full backup under the hood
The source takes a fresh full backup, ships it in 2 MB chunks over the target’s ops-token channel, and the target auto-restores it.
Optional plugin safety net
One checkbox deactivates every plugin except the Agent on the target, so a runtime mismatch cannot turn the first boot into a critical-error page.
When to reach for it
- Moving hosts. Stand up an empty WordPress on the new host, install the Agent, migrate the old site onto it, then flip DNS.
- Refreshing a staging site from production. Production is the source, staging is the target. Staging keeps its own
staging.address. - Pushing an approved rebuild live. The rebuild site is the source, the live site is the target. Back the live site up first.
- Cloning a site as a starting point for a new client build.
When not to
- You only want to change a site's domain. That is a different tool — URL Replacement. Migration will not change an address.
- You want a continuously-synced standby. That is Live Replication (Continuity), which keeps the target's own operator login intact. Migration is one-shot and replaces users.
- You want to move a single post or page. Use the content editor on the site detail page instead.
Addresses: what changes and what does not
Content moves. The address does not.
A migration copies the source site's content into the target site. The target keeps its own address. The source is not modified at all.
https://old-host.example.com
Unchanged. Nothing is written to the source beyond a log line and a temporary backup archive that is deleted once it has been shipped.
https://new-host.example.com
Same address, source's content. Its old content is gone; its URL is not.
What the engine does, step by step
This is not a convention or a best-effort heuristic — it is written into the restore path, and it is written more than once on purpose.
- The target decides its own restore URL. When the target finalises an incoming migration it reads its own
siteurland restores against that. The source cannot pass a URL for the target to adopt. - Immediately after the database import, the engine writes the target's URL back in. The restored dump carries the source's
wp_optionsrows, so the very next statement is:
The value is the target's own URL. Never the source's.UPDATE wp_options SET option_value = <target url> WHERE option_name IN ('siteurl','home') - Then a search-replace phase rewrites the content. URLs inside the content are not left pointing at the source. The engine builds a pair list (source URL → target URL, plus
http/httpsvariants, URL-encoded and JSON-escaped forms, the uploads base URL, and the on-disk paths for uploads, plugins, themes andABSPATH) and walks every text column in every table, batching rows so it never loads a table into memory. Post content, media paths and serialized option values are all covered — serialized data is unserialized, rewritten and re-serialized so string lengths stay valid, which is why Elementor layouts and theme mods survive. - At the end of the restore the URL is written a third time and verified. Some caching and licensing plugins flush a stale
alloptionscache back to the database during a long restore. The engine re-runs theUPDATE, invalidates the caches, readssiteurlback, forces it if it does not match, and registers a shutdown hook that writes it once more so the last word belongs to the restore.
The net effect: after a migration the target answers on its own domain, its links and media point at its own domain, and the source is exactly as you left it.
Changing a site's own address is a separate tool
If what you actually want is “this site should now live at a different domain”, migration is the wrong instrument. Use URL Replacement — site detail page → URL Replacement. It runs the same serialized-safe replace engine, but it is aimed at one site rather than at moving content between two:
- From URL and To URL — it pre-fills From with the site's current
siteurl. - An
also_update_siteurloption (on by default from the Manager) that flipssiteurlandhometo the new value — but only when the currentsiteurlactually matches what you typed in From. - A real dry-run mode. Press Dry run and you get scanned tables, scanned rows, rows that would be updated, cells that would change, the elapsed time, and up to five before/after samples — with nothing written. Rehearse the address change, read the blast radius, then press Apply.
- It refuses to rewrite the Agent's own option rows (activation token, hub URL, pair status), so a careless replace cannot sever the Manager link.
A common host move is therefore two moves, in this order: migrate content onto the new host while it still has its own staging address, verify it, flip DNS, then run URL Replacement to move the address itself.
The one-line version. Migration moves content between two sites and leaves both addresses alone. URL Replacement changes one site's address and leaves its content in place. If you are reaching for migration to rename a domain, stop.
Before you start
- Both sites are paired. Source and target must both show Paired in the Manager. The Manager hands the target's ops token to the source so the source can push chunks; if the target has no usable token the request is rejected with “target has no ops token to accept the migration (re-pair the target)” before anything is copied.
- The Agent is healthy on both. Both sites need the WP Tender Agent installed, activated and reporting live status. A site whose card shows no CPU/RAM/disk figures is not answering the Manager and will not answer a migration either.
- Migration is a paid feature. The Migration tab and the Migrate button are gated on
sites.site_to_site_migration, which every paid plan enables. On Free the tab shows an upgrade prompt instead. - Take a backup of the target first. Target site → Backup & restore → Trigger → Full. Migration overwrites the target and there is no undo; this backup is your only rollback path.
- Check disk headroom on both. A full backup of a typical site is 80–100 MB, and it exists twice mid-flight — once in
wp-content/uploads/wptp-backups/on the source, once inwp-content/uploads/wptp-migrated/on the target. Both copies are pruned automatically when the migration completes. - Source and target must be different sites. The endpoint rejects a request where they are the same.
Running a migration
Two entry points, same engine behind both.
From the dashboard (pick both ends)
- Dashboard header → Migrate. The wizard has three steps: Source, Target, Review.
- Source — “Pick the source site (data lives here)”. Only paired sites are listed; sites without the Agent surface are shown greyed out with a no Agent badge.
- Target — “Pick the target site (data will be replaced)”. The source you just picked is excluded from the list.
- Review — shows Source, Target, and Mode: overwrite, with the note “DB + media on target are replaced”. It also renders the runtime-compatibility panel and the deactivate-plugins option described below.
- Press Start migration. You get a toast with the job id and the wizard closes.
From a site's Migration tab (this site is the source)
- Open the source site → Migration.
- Pick a Target site from the dropdown. It lists every other paired site that exposes the backup/restore surface.
- Start it. The card also shows this site's migration history, refreshed every 10 seconds.
The runtime compatibility check
On the Review step the wizard compares the two sites on the two axes that account for almost every “the migration restored but the site bricked” report: PHP major version and database major version (it also flags a MySQL ↔ MariaDB engine swap). It is informational — a mismatch never blocks the migration, it just tells you that the source's plugins are about to boot on a runtime they have never seen.
The plugin-deactivation safety net
On the Review step there is an opt-in checkbox: “Migration sonrası hedef sitedeki plugin’leri devre dışı bırak” — deactivate the target's plugins after the migration. It is off by default and the wizard visually recommends it when the compatibility check came back with a warning.
With it on, immediately after the restore lands the target:
- Reads the
active_pluginslist it just inherited from the source and deactivates every plugin except the WP Tender Agent (the legacy split plugin slugs are kept too, on fleets that still run them). - Writes a log line saying how many plugins were deactivated, so you know what to expect when you open WP-Admin.
Why it exists: the restored wp_options brings the source's active-plugin list, and some of those plugins may depend on a PHP extension, a PHP version or a database engine the target does not have. One incompatible plugin fatals at boot and turns the target into an unrecoverable “critical error” page. With the toggle on you get a working WP-Admin login screen instead, and you reactivate plugins one at a time once you have confirmed the runtime is happy.
Turn it on when PHP or database majors differ between source and target. Leave it off when the two runtimes match and you want the target usable the moment the migration finishes.
There is no dry run
Migration has no rehearsal mode. Older mobile and desktop builds shipped a Dry run toggle that the Agent never implemented — the parameter was silently stripped, so the UI promised a rehearsal while a real, overwriting migration ran against the target.
That is fixed by refusing rather than pretending. The Manager now accepts the field only so it can reject it: a client that asks for a dry-run migration gets “Dry-run migration is not supported. Turn off ‘Dry run’ to start a real migration.” and a 400, and nothing is copied. Clients already in the field keep working with the toggle off.
If you need to rehearse a change, rehearse the part that has a rehearsal: URL Replacement has a real dry run. For migration itself, the substitute is a full backup of the target.
What happens on the target
In order, once the last chunk has landed:
- Assemble. The chunks are concatenated back into a single archive under
wp-content/uploads/wptp-migrated/(which carries aDeny from allguard). A missing chunk fails here, before anything is overwritten. - Snapshot the pairing state. Before the restore wipes
wp_options, the target saves its own Manager pairing and replication keys — activation token, hub URL, pair status and permissions, plus the Continuity local token, peer URL, peer token, role and sync settings. - Import the database in timed chunks, fixing the table prefix if the two sites use different ones (this also rewrites the
wp_capabilities/wp_user_levelmeta keys, so admin rights survive a prefix change). - Write the target's
siteurlandhome— see Addresses. - Search-replace across every text column, source URL and paths → target URL and paths.
- Clean up things that belong to the source. Scheduled backup rows are cleared unconditionally, so the source's schedules never fire on the target. Everything else in this step is gated on the restore URL differing from the source URL — which is the ordinary case for a site-to-site migration, but not for a same-URL restore, where none of it runs. Only when the two URLs differ does the engine delete cloud OAuth tokens, OAuth state transients and stale hub-pairing rows (carrying those across would point the target's cloud uploads at the source operator's storage account), drop the Rank Math instant-indexing key, and deactivate the handful of plugins that break on a URL change (Really Simple SSL, WP Force SSL, WPS Hide Login, Hide My WP).
- Restore the pairing state saved in step 2, so the site does not go “unpaired” in your Manager the moment a migration lands.
- Clear authentication. Auth cookies are cleared and every
session_tokensrow is deleted. See Users and sessions. - Flush caches and permalinks. The restored
rewrite_rulesoption references the source's host, and a stale one makes/wp-json/*return 404 — which would break the Agent's REST channel and every later migration. Ifpermalink_structurecame across empty it is set to/%postname%/, then rewrite rules are flushed. - Optionally deactivate plugins, if you ticked the box — see above.
- Prune. The migration archive is deleted from
wptp-migrated/, and the source deletes its own copy too. Without this, every cycle would leave another 80 MB+ artefact behind for a future full backup to swallow.
Users and sessions
Migration is a full replacement, and that includes accounts. The target receives every wp_users and wp_usermeta row from the source verbatim. After the migration, you log into the target with the source's credentials; the target's pre-migration accounts are not there any more. This matches the mental model the rest of the market uses — after a migration the new host is the source.
Sessions are then deliberately cleared. Your browser still holds a cookie tied to the pre-migration user id, and after the restore that id maps to a different user or to nobody — which surfaces as the “you do not have permission to access this page” screen in /wp-admin. Clearing auth cookies and every session_tokens row forces a clean login, which is the only deterministic recovery. Expect to be logged out of the target, and expect to log back in with source credentials.
If you need the target's own operator login to survive, migration is the wrong surface — Live Replication (Continuity) preserves it.
What survives on the target
| On the target | After the migration |
|---|---|
| Address (siteurl / home) | The target's own, written explicitly after the import and verified at the end of the restore. |
| Posts, pages, media | Replaced by the source's, with every source URL and file path rewritten to the target's. |
| Users and passwords | Replaced by the source's. You log in with source credentials; the target's old accounts are gone. |
| Logged-in sessions | Cleared. Auth cookies are dropped and every session_tokens row is deleted, so everyone re-authenticates. |
| Manager pairing | Preserved. Saved before the restore and written back afterwards, so the site does not go unpaired. |
| Live Replication pairing | Preserved the same way — peer URL, peer token, role and sync settings all survive the round trip. |
| Active plugins | The source's list, unless you ticked the deactivation option — in which case everything but the Agent is switched off. |
| Backup schedules | Cleared, so the source's schedules do not start firing on the target. |
| Cloud destinations | Cleared whenever the target's URL differs from the source's — the ordinary case for a migration. Carrying the source's OAuth tokens over would upload the target's backups into someone else's storage account. On a same-URL restore they are left in place. |
| Permalinks | Re-flushed, and defaulted to /%postname%/ if the imported value was empty, so /wp-json keeps answering. |
How long it takes, and why
A migration is three serial phases, and the slow one is usually the network hop in the middle.
- Backup on the source. A fresh full backup, labelled
hub-mig-<job id>so you can tie the archive back to the job. Time scales with site size and the source host's CPU and disk. - Transfer. The archive is sent in 2 MB chunks, each its own authenticated POST with a 120-second window. A typical 80 MB site is around 40 chunks. This is why migrations are slower than a straight file copy — and why they survive hosts that kill long requests. Progress is logged every ten chunks with a percentage.
- A stalled chunk is retried, so give it time. If a chunk POST comes back with a transient network error — a timeout, a cURL 28, a connection reset — the source retries it, up to three attempts with 0, 3 and 9 seconds of backoff between them. Any other error fails the migration straight away rather than burning the retries. Worst case, a single badly stuck chunk holds the transfer for about six minutes (three 120-second windows plus the backoff) before the job gives up. If you are watching a transfer that looks frozen, that is the window to wait out before assuming it is dead.
- Restore on the target. The database import and the search-replace both run in timed chunks so a shared host's PHP timeout cannot kill them mid-table, but they are budgeted differently. The import replays the dump statement by statement, resuming from a saved byte offset, against a 15-second budget per tick. The search-replace walks 200 rows at a time against a 14-second budget. On a big site the search-replace pass over every text column in every table is often the longest single step.
Small brochure sites finish in a couple of minutes. A media-heavy WooCommerce site on shared hosting can take considerably longer — the wizard does not estimate, and the honest guidance is to start it when you can leave it alone and watch the Jobs page.
Watching progress
- Jobs. The migration is registered as a job of type
migrationagainst the source site, with the remote job id in its payload. The Jobs page and the active-jobs popover in the dashboard header both track it live. - The source's Migration tab keeps a history list of migrations that started there, polled every 10 seconds.
- Log Manager, on either site. Both ends write structured log lines under the
migrationfeature. On the source look formigration-send-started; on the target,migration-transfer-started,migration-receiving(every ten chunks, with a percentage),migration-transfer-complete,migration-restore-startedandmigration-restore-complete. Filter by feature migration to see the whole run in one column. - Site events. The Manager writes an event on the source when the migration is queued, naming the target and the remote job id.
Reading a failure
Failures name their phase, which is most of the diagnosis. The log action tells you how far it got:
- Nothing logged on either side — the Manager rejected the request before it reached WordPress. The toast carries the reason (not paired, no ops token, same source and target, dry run requested, no permission on one of the two sites).
- Send started, no receive — the source could not reach the target's REST endpoint. Firewall, WAF or DNS between the two hosts.
Missing chunk N— the transfer was interrupted. Nothing was overwritten; the target refuses to assemble an incomplete archive. Re-run it.migration-restore-error— the archive landed and the restore failed. The message carries the engine's own error. This is the one case where the target may be left mid-restore; restore the target from the backup you took beforehand.- Restore complete but the site errors — a plugin from the source is incompatible with the target's runtime. Re-run with deactivate plugins ticked.
Troubleshooting
“both sites must be paired”
One of the two is not in paired state. Open each site's detail page and confirm live status is being reported. If a site went unpaired, re-paste the activation token in WP admin → Settings → WP Tender.
“target has no ops token to accept the migration (re-pair the target)”
The Manager could not read a usable ops token for the target, so the source would have nothing to authenticate its chunk uploads with. Re-pair the target and try again. Nothing was copied.
“source WP Tender Agent not available”
The Manager has no working client for the source. Usually the plugin is deactivated, or the site's REST route is blocked. Check /wp-json/wpt/* is reachable — see the Agent troubleshooting notes.
The target 404s on /wp-json after a migration
Stale rewrite rules. The restore flushes them automatically, but a host with an aggressive object cache can serve the old set for a while. Open Settings → Permalinks on the target and press Save to force a flush.
The target lost its plugins
Two different things look alike here. If you ticked deactivate plugins, they are installed and inactive — turn them back on one at a time from WP-Admin. If you did not tick it and only the Agent is active, check the migration log on the target: the restore explicitly invalidates the alloptions and active_plugins caches after the import so it cannot write a pre-restore plugin list back over the source's.
Both sites are on the same domain / the URLs look wrong
Read Addresses again first — a migration does not move a domain, so if the target now answers on the source's URL, something other than the migration set it (a hard-coded WP_HOME / WP_SITEURL in wp-config.phpoverrides the database and the restore cannot win against it). Check wp-config.php on the target, then use URL Replacement with a dry run to see what is still holding the old value.
Disk filled up mid-migration
Both ends prune the archive on success, but a failed run can leave one behind. Look in wp-content/uploads/wptp-backups/ on the source and wp-content/uploads/wptp-migrated/ on the target and delete stale zips.
I need to roll the target back
Restore the full backup you took before starting: target site → Backup & restore → pick the pre-migration archive → restore. There is no built-in undo, which is why the pre-flight backup is the first item on the checklist.
See also: Using the manager · WP Tender Agent · FAQ.