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.
Should you make this move?
Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.
MySQL
- Ubiquity, operational familiarity, and broad hosting support reduce adoption risk
- A mature data model and query ecosystem supports serious production workloads
- Advanced SQL, extensibility, and consistency features can lag PostgreSQL
- Schema design, migration, tuning, and operations require specialist attention
PlanetScale
- 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
- 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
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.
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
Know the shape of the move.
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.
What survives the move.
“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.
| Item | Outcome | Impact | What happens | Mitigation |
|---|---|---|---|---|
| Server settings | manual | critical | PlanetScale 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 user | manual | critical | One 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 key | partial | critical | Every 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 partitions | partial | high | Only 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 keys | partial | critical | If 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 indexes | partial | medium | Without 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 import | manual | critical | PlanetScale 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 users | partial | high | SHOW 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 switch | manual | critical | MoveTables 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 workflow | manual | critical | move-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 copy | manual | high | PlanetScale 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. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| MySQL database | PlanetScale keyspace | transform | PlanetScale suggests the same database name so app config changes less. |
| External host | External keyspace | manual | Name it differently from the PlanetScale keyspace, such as mydb_source. |
| InnoDB tables | MoveTables target | automatic | Use --all-tables, or --tables, or --all-tables with --exclude-tables. |
| Unique not-null key | Required key | manual | Add one or exclude the table. |
| Foreign keys | PlanetScale foreign keys | transform | --all-tables and --atomic-copy. No --defer-secondary-keys. |
| Secondary indexes | Deferred or immediate indexes | transform | Defer only when there are no foreign keys. |
| Binary log position | Running MoveTables stream | automatic | Keep at least 48 hours of binlogs. |
| MySQL user | PlanetScale password | manual | The import user is not the app user. |
| Writes after the switch | Reversed replication, then complete | manual | The import user needs write grants so PlanetScale can replicate back until you complete. |
Make the move recoverable.
Create the source-of-truth backup
Keep a MySQL copy you can reopen if MoveTables is wrong.
- Confirm binlog retention is at least 48 hours, then take a dump or snapshot.
- Record SHOW VARIABLES for gtid_mode, log_bin, binlog_format, and binlog_row_image.
- Run create-external with --dry-run and save the output.
- 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.
External keyspace
Connect PlanetScale to MySQL only after the server check passes.
- Create the Vitess database in a region near the source, with enough storage.
- Dry-run the external keyspace. Fix grants, GTID, binlogs, and schema flags.
- Create it with --ssl-mode required and --wait.
- 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.
MoveTables
Copy tables, replicate, switch traffic, and finish without dropping the source.
- create with --all-tables. Add --defer-secondary-keys only when there are no foreign keys. Add --atomic-copy when there are.
- status until the streams are running and lag is acceptable.
- Point a test app at PlanetScale. Switch reads, then writes.
- 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.
The things most likely to hurt.
These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.
The dry run is skipped
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.
DDL trips STOP
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.
complete drops the wrong data
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.
Do the work in this order.
- 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 checkpointDry run?
Go when: Every table is InnoDB with a unique key, or it is excluded on purpose.
Stop when: GTID cannot be enabled.
- 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 checkpointCreate 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.
- 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 checkpointSwitch reads?
Go when: Status is healthy and a sample row matches.
Stop when: The workflow is stopped on DDL you have not reviewed.
- 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 checkpointPlanetScale 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.
- 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 checkpointClose rollback?
Go when: The app cycle matches and --keep-data=true was the flag you ran.
Stop when: Writes are still landing on MySQL.
Cut over with a way back.
Cutover
Move reads and then writes to PlanetScale without dropping MySQL tables.
- Confirm MoveTables is replicating and DDL is frozen.
- Switch reads and run the test queries.
- Switch writes.
- complete with --keep-data=true and --keep-routing-rules=false.
- 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.
Rollback
Put writes back on MySQL if PlanetScale is not ready, and keep the copied data decision explicit.
- Stop the app from writing if both primaries are receiving traffic.
- Switch writes back to MySQL if the workflow still allows it.
- Export any rows that landed only on PlanetScale.
- Review those rows before inserting them on MySQL.
- 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.
- 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
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
V-01Blocking | GTID and binlogs | Read 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-02Blocking | Import user | Compare grants with PlanetScale's table. | Replication and DML grants are present, including ps_import_*. | SHOW GRANTS. | |
V-03Blocking | Flagged tables | Read the schema check for unique keys, engine, charset, and partitions. | Every flagged table is fixed or named in --exclude-tables. | Schema check. | |
V-04Blocking | Command matches constraints | Confirm --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-05Blocking | Sample counts | Compare counts on the largest tables after replication lag is flat. | Counts match. | Count sheet. | |
V-06Blocking | One primary | Insert a row through the app. | The row appears on PlanetScale and not as a new source-primary write. | Row check. | |
V-07Blocking | Source tables kept | Read the complete command. | --keep-data=true and --keep-routing-rules=false. | Shell history. | |
V-08 | No unreviewed ALTER | Ask if the workflow stopped, and why it was started again. | STOP was reviewed before restart. | Workflow log. |
Remove the scaffolding safely.
Safe after: One app cycle on PlanetScale, with MySQL tables still intact and routing rules removed.
- Reset vreplication_copy_phase_duration if you changed it.
- Delete the external keyspace when reverse replication is no longer required.
- Revoke the import user's grants.
- Keep the MySQL dump until rollback closes.
- Decommission MySQL only after that window.
- Schedule the next PlanetScale backup review.
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.
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.
- 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.
- 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.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- PlanetScale: database importsAccessed 2026-11-04
- PlanetScale: import user permissionsAccessed 2026-11-04
- PlanetScale: move-tablesAccessed 2026-11-04
- PlanetScale: import troubleshootingAccessed 2026-11-04