← All playbooks
Databases migration

MySQL → PlanetScale

Import an internet-facing MySQL 5.6, 5.7, or 8.0 database into PlanetScale Vitess with an external keyspace and MoveTables. Foreign keys, missing unique keys, and DDL during the copy change the command you run.

Typical timeline3 to 10 days10 to 24 hours active work
Statusneeds review
Source profileMySQL settings required by PlanetScale's import guide, reviewed 2026-11-04
Destination profilePlanetScale Vitess database import docs reviewed 2026-11-04
Documentation date
Review window
Evidence4 sources
This route needs review.Use it as a planning baseline, then verify the dated sources and test with representative data before production.
Before you migrate

Should you make this move?

Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.

Current platform

MySQL

Reasons to stay
  • Ubiquity, operational familiarity, and broad hosting support reduce adoption risk
  • A mature data model and query ecosystem supports serious production workloads
Reasons to leave
  • Advanced SQL, extensibility, and consistency features can lag PostgreSQL
  • Schema design, migration, tuning, and operations require specialist attention
New platform

PlanetScale

What gets better
  • Branching, deploy requests, and Metal performance make Vitess MySQL or managed Postgres appealing for production apps
  • A mature data model and query ecosystem supports serious production workloads
What gets worse
  • Engine choice, Vitess limits, and paid-plan economics change how you dump, branch, and cut over
  • Schema design, migration, tuning, and operations require specialist attention
Best of the move

PlanetScale: Branching, deploy requests, and Metal performance make Vitess MySQL or managed Postgres appealing for production apps. This removes a major source-side concern: Advanced SQL, extensibility, and consistency features can lag PostgreSQL.

Worst of the move

What you lose: Ubiquity, operational familiarity, and broad hosting support reduce adoption risk. What you inherit: Engine choice, Vitess limits, and paid-plan economics change how you dump, branch, and cut over.

Jump to a section
01At a glance

Know the shape of the move.

Transfer outcome11 features audited
Transfer outcome distributionClean transfer: 0, Partial transfer: 5, Manual rebuild: 6, Not transferred: 0.
Clean0
Partial5
Manual6
Lost0
Mapping route9 of 9 fields have a destination path

This timeline assumes

  • The source is MySQL 5.6, 5.7, or 8.0 on the public internet, or a host whose allow list can include PlanetScale. PlanetScale Postgres is a different playbook.
  • You are an Organization Administrator or a Database Administrator. A service token needs create_branch, read_workflow, write_workflow, and delete_workflow, plus delete_production_branch to remove the external keyspace.
  • You can set GTID, binary logging, and binlog retention, and you can avoid DDL until the workflow is complete.
  • The site already documents MySQL → PostgreSQL and PlanetScale → PostgreSQL if you are leaving MySQL, and PostgreSQL → PlanetScale if the source is already Postgres.
02What transfers

What survives the move.

“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.

ItemOutcomeImpactWhat happensMitigation
Server settingsmanualcriticalPlanetScale will not create the external keyspace unless gtid_mode and log_bin are ON, binlog_format is ROW, and binlog_row_image is FULL or NOBLOB. binlog_expire_logs_seconds must be at least 172800, or expire_logs_days must cover that. On RDS and Aurora, set binlog retention hours to at least 48. The source must not already contain a _vt database.Run pscale keyspace create-external --dry-run. Fix the settings it rejects.
Import usermanualcriticalOne user reads schema, copies rows, replicates, and, after you switch primary traffic, replicates writes back. PlanetScale checks PROCESS, REPLICATION SLAVE, REPLICATION CLIENT, and RELOAD. On MySQL 8.0, FLUSH_TABLES can replace RELOAD. The database needs SELECT, INSERT, UPDATE, DELETE, LOCK TABLES, and SHOW VIEW if you have views. ps_import_* needs those DML grants plus CREATE, DROP, and ALTER.Create that user before the dry run. Do not use a read-only account.
Tables without a unique keypartialcriticalEvery imported table needs a unique, not-null key. Schema errors are listed per table and do not block keyspace creation. You can fix the table or pass --exclude-tables.Add a key, or exclude the table and move it another way.
Storage engine, charset, and partitionspartialhighOnly InnoDB is supported. Charsets are utf8, utf8mb4, utf8mb3, latin1, and ascii. Only RANGE partitioning is supported, without subpartitions.Convert or exclude flagged tables before MoveTables.
Foreign keyspartialcriticalIf the source uses foreign keys, PlanetScale turns foreign key support on when you create the external keyspace. Import every table with --all-tables and no --exclude-tables. Pass --atomic-copy. Do not pass --defer-secondary-keys, because the constraints need their indexes during the copy. An atomic copy holds a long transaction and starts over if it fails.Set vttablet.vreplication_copy_phase_duration to 24h for foreign keys. Reset that parameter when you are done.
Secondary indexespartialmediumWithout foreign keys, --defer-secondary-keys builds secondary indexes after the copy. PlanetScale says this is often 2 to 3 times faster on tables with several indexes. With foreign keys, do not use the flag.Pick one command. Do not mix --defer-secondary-keys with --atomic-copy.
DDL during the importmanualcriticalPlanetScale says to avoid CREATE, DROP, ALTER, and TRUNCATE on both sides. --on-ddl defaults to STOP, which halts the workflow when a source schema change appears. IGNORE skips the change and the schemas diverge. EXEC applies it and stops if Vitess rejects it. EXEC_IGNORE keeps going after a failure.Leave the default STOP. Restart with move-tables start only after you understand the change.
Views, routines, and userspartialhighSHOW VIEW is required when views exist, which means views are visible to the check. MySQL accounts, grants, and events are not rows MoveTables copies into the app role you will use on PlanetScale.Create the PlanetScale password the app will use. Recreate events in the app or as scheduled jobs.
Traffic switchmanualcriticalMoveTables copies, then replicates, while the source still serves traffic. You switch reads, then writes. The app can point at PlanetScale before the write switch, as a test.Read move-tables status. Switch reads first. Switch writes only after a test query matches.
Completing the workflowmanualcriticalmove-tables complete requires --keep-data=true for an import, so source tables are not dropped or renamed. --keep-routing-rules=false removes rules that still sent queries to the external keyspace. A space-separated --keep-data false is read as --keep-data=true. Write the flag with an equals sign.Preview the complete command. Use --keep-data=true and --keep-routing-rules=false.
Canceling a bad copymanualhighPlanetScale says --keep-data=false on complete deletes the data already copied into the PlanetScale keyspace. The external keyspace stays connected so you can start again. Delete that keyspace if you will not retry.Do not run that flag against a database the app is already using.
03Field and feature mapping

Where each thing goes.

SourceDestinationMethodNotes
MySQL databasePlanetScale keyspacetransformPlanetScale suggests the same database name so app config changes less.
External hostExternal keyspacemanualName it differently from the PlanetScale keyspace, such as mydb_source.
InnoDB tablesMoveTables targetautomaticUse --all-tables, or --tables, or --all-tables with --exclude-tables.
Unique not-null keyRequired keymanualAdd one or exclude the table.
Foreign keysPlanetScale foreign keystransform--all-tables and --atomic-copy. No --defer-secondary-keys.
Secondary indexesDeferred or immediate indexestransformDefer only when there are no foreign keys.
Binary log positionRunning MoveTables streamautomaticKeep at least 48 hours of binlogs.
MySQL userPlanetScale passwordmanualThe import user is not the app user.
Writes after the switchReversed replication, then completemanualThe import user needs write grants so PlanetScale can replicate back until you complete.
04Before you begin

Make the move recoverable.

Backup procedure

Create the source-of-truth backup

Keep a MySQL copy you can reopen if MoveTables is wrong.

  1. Confirm binlog retention is at least 48 hours, then take a dump or snapshot.
  2. Record SHOW VARIABLES for gtid_mode, log_bin, binlog_format, and binlog_row_image.
  3. Run create-external with --dry-run and save the output.
  4. Leave the source online until you have completed the workflow and closed rollback.

Proof to capture: The dry-run output, the variable list, and the dump or snapshot id are in the migration folder.

Transformation · pscale keyspace create-external

External keyspace

Connect PlanetScale to MySQL only after the server check passes.

  1. Create the Vitess database in a region near the source, with enough storage.
  2. Dry-run the external keyspace. Fix grants, GTID, binlogs, and schema flags.
  3. Create it with --ssl-mode required and --wait.
  4. If you use foreign keys, set vreplication_copy_phase_duration to 24h.

Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.

Transformation · pscale branch vtctl move-tables

MoveTables

Copy tables, replicate, switch traffic, and finish without dropping the source.

  1. create with --all-tables. Add --defer-secondary-keys only when there are no foreign keys. Add --atomic-copy when there are.
  2. status until the streams are running and lag is acceptable.
  3. Point a test app at PlanetScale. Switch reads, then writes.
  4. complete with --keep-data=true and --keep-routing-rules=false.

Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.

05Handle with care

The things most likely to hurt.

These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.

Backup

The dry run is skipped

criticalpossible likelihood

create-external fails on GTID, grants, or connectivity.

Consequence
The import never starts, or it starts against the wrong host.
Mitigation
Treat a failed dry run as the stop.

Stop if: The source has a _vt database or binlogs shorter than 48 hours.

Pilot

DDL trips STOP

highpossible likelihood

An ALTER on the source stops the workflow.

Consequence
Replication lags until someone restarts it, and the schemas may differ.
Mitigation
Keep the default --on-ddl STOP and freeze migrations.

Stop if: Someone wants EXEC_IGNORE so the copy ignores a failed ALTER.

Cutover

complete drops the wrong data

criticalunlikely likelihood

The flag is --keep-data=false, or the equals sign is missing and you think it means false.

Consequence
PlanetScale deletes copied data, or you keep data you meant to drop on a retry.
Mitigation
Preview the command. For a real cutover, pass --keep-data=true.

Stop if: You cannot say which keyspace the flag will touch.

06Precise timeline

Do the work in this order.

Estimate forOne internet-facing MySQL database that already uses InnoDB and GTID, or can be changed to them
Total elapsed3 to 10 days
Active work10 to 24 hours
BufferForeign keys force an atomic copy that restarts from the beginning if it fails. Add time for that restart.
01
Days 1 to 2Settings and keys1 to 2 days
02
Days 2 to 3Dump and dry run1 day
03
Days 3 to 7MoveTables while MySQL serves1 to 4 days
04
Days 7 to 8Reads, then writes1 day
05
Days 8 to 12Watch and remove the keyspace2 to 4 days
  1. Days 1 to 2 · inventory

    Settings and keys

    3 to 6 hours active1 to 2 days elapsedA maintenance window if GTID requires a restart waiting
    • List engines, charsets, unique keys, and foreign keys.
    • Plan the import user.

    Depends on: MySQL admin and a PlanetScale org role

    Stop / go checkpoint

    Dry run?

    Go when: Every table is InnoDB with a unique key, or it is excluded on purpose.

    Stop when: GTID cannot be enabled.

  2. Days 2 to 3 · backup

    Dump and dry run

    2 to 4 hours active1 day elapsedBinlog retention change waiting
    • Take a dump.
    • Run create-external --dry-run.

    Depends on: Settings plan

    Stop / go checkpoint

    Create the keyspace?

    Go when: The dry run passes grants, connectivity, and server settings.

    Stop when: A schema error belongs to a table you meant to import.

  3. Days 3 to 7 · pilot

    MoveTables while MySQL serves

    3 to 8 hours active1 to 4 days elapsedCopy and replication waiting
    • Start the workflow.
    • Read status.
    • Query PlanetScale from a test app.

    Depends on: External keyspace

    Stop / go checkpoint

    Switch reads?

    Go when: Status is healthy and a sample row matches.

    Stop when: The workflow is stopped on DDL you have not reviewed.

  4. Days 7 to 8 · cutover

    Reads, then writes

    2 to 4 hours active1 day elapsedReplication catch-up waiting
    • Switch reads.
    • Switch writes.
    • complete with --keep-data=true.

    Depends on: Passed pilot

    Stop / go checkpoint

    PlanetScale owns writes?

    Go when: A new row is written on PlanetScale and the source is no longer primary.

    Stop when: Routing rules still send writes to the external keyspace.

  5. Days 8 to 12 · observe

    Watch and remove the keyspace

    2 to 4 hours active2 to 4 days elapsedOne app cycle waiting
    • Watch errors.
    • Delete the external keyspace when you will not reverse replication.

    Depends on: complete

    Stop / go checkpoint

    Close rollback?

    Go when: The app cycle matches and --keep-data=true was the flag you ran.

    Stop when: Writes are still landing on MySQL.

07The point of change

Cut over with a way back.

Go live

Cutover

Move reads and then writes to PlanetScale without dropping MySQL tables.

Recommended window: A low-traffic window with the database owner watching status.

  1. Confirm MoveTables is replicating and DDL is frozen.
  2. Switch reads and run the test queries.
  3. Switch writes.
  4. complete with --keep-data=true and --keep-routing-rules=false.
  5. Reset vreplication_copy_phase_duration if you raised it for foreign keys.

Proof to capture: Status shows the workflow complete. A new row exists on PlanetScale. MySQL tables are still present.

Return to safety

Rollback

Put writes back on MySQL if PlanetScale is not ready, and keep the copied data decision explicit.

Deadline: Before you delete the external keyspace or drop the MySQL user that replicates back.

  1. Stop the app from writing if both primaries are receiving traffic.
  2. Switch writes back to MySQL if the workflow still allows it.
  3. Export any rows that landed only on PlanetScale.
  4. Review those rows before inserting them on MySQL.
  5. Leave the source tables in place. --keep-data=true was what preserved them.

Proof to capture: MySQL is the only writer, and PlanetScale-only rows are listed.

Rollback immediately when
  • A table you need was excluded
  • DDL stopped the workflow and the schemas differ
  • A foreign-key copy fails and restarts without a clear lag
  • The app writes to both databases
  • complete was run with a flag you did not preview
08Verification report

Prove the migration worked.

Every blocking check must pass. Capture the evidence before cleanup begins.

0%
Interactive report preview0 / 8 checks passed
PassIDCheckMethodExpected resultEvidence
V-01BlockingGTID and binlogsRead the dry-run result and SHOW VARIABLES.gtid_mode and log_bin are ON, format is ROW, retention is at least 48 hours.Dry-run output.
V-02BlockingImport userCompare grants with PlanetScale's table.Replication and DML grants are present, including ps_import_*.SHOW GRANTS.
V-03BlockingFlagged tablesRead the schema check for unique keys, engine, charset, and partitions.Every flagged table is fixed or named in --exclude-tables.Schema check.
V-04BlockingCommand matches constraintsConfirm --atomic-copy and the absence of --defer-secondary-keys when foreign keys exist.The create command matches the foreign-key block in the import guide.Command log.
V-05BlockingSample countsCompare counts on the largest tables after replication lag is flat.Counts match.Count sheet.
V-06BlockingOne primaryInsert a row through the app.The row appears on PlanetScale and not as a new source-primary write.Row check.
V-07BlockingSource tables keptRead the complete command.--keep-data=true and --keep-routing-rules=false.Shell history.
V-08No unreviewed ALTERAsk if the workflow stopped, and why it was started again.STOP was reviewed before restart.Workflow log.
09Post-migration cleanup

Remove the scaffolding safely.

Safe after: One app cycle on PlanetScale, with MySQL tables still intact and routing rules removed.

  1. Reset vreplication_copy_phase_duration if you changed it.
  2. Delete the external keyspace when reverse replication is no longer required.
  3. Revoke the import user's grants.
  4. Keep the MySQL dump until rollback closes.
  5. Decommission MySQL only after that window.
  6. Schedule the next PlanetScale backup review.
10Experiences from the field

When the plan met reality.

First-hand accounts are preferred. Vendor case studies are labeled, and every note below is an editorial paraphrase—follow the link for the full context.

First-person accounttetsuya28 · Zenn

Migrating a personal Aurora MySQL database to PlanetScale2023

tetsuya28 moved a personal app's Aurora MySQL database, a db.t3.small shared with other apps and costing about 4,000 yen a month, to PlanetScale. The app runs on Vercel and Cloud Run in asia-northeast1. He stopped ent AutoMigrate because PlanetScale would not take DDL on the main branch, and he applied schema through a branch and a deploy request. He removed foreign keys because, as he wrote in 2023, Vitess did not support them. That limit is historical. PlanetScale's 2026 import guide turns foreign key support on when you create the external keyspace. He also had to connect with TLS. PlanetScale's Aurora import was in beta as of 31 December 2022 and worked after he changed Aurora options. Latency then got worse because the database was in us-east-1 while the API was in asia-northeast1. The Hobby plan he was on allowed one database, so a region move meant another migration. Do not treat that free Hobby plan as current pricing.

What they recommend
  • Put the PlanetScale database in a region near the application before you judge latency.
  • Treat a 2023 foreign-key removal as history. Follow the current import guide if the source still has foreign keys.
Worth noticing
  • Schema changes moved from startup DDL to a branch and a deploy request because the main branch rejected them.
  • The slowdown after cutover was a region mismatch, us-east-1 against Cloud Run in asia-northeast1.
Sources and evidence

Verify against the primary material.

Platform behavior changes. Check these sources and the review dates above before executing a production migration.

  1. PlanetScale: database importsAccessed 2026-11-04
  2. PlanetScale: import user permissionsAccessed 2026-11-04
  3. PlanetScale: move-tablesAccessed 2026-11-04
  4. PlanetScale: import troubleshootingAccessed 2026-11-04