# PlanetScale Best Practices: Schema Changes, Limits and Connections

URL: https://www.devopsness.com/blog/planetscale-best-practices
Author: Kiril Urbonas
Published: 2026-11-11
Updated: 2026-11-11
Categories: Cloud
Tags: Planetscale, Mysql, Database, Migration, Serverless

Summary: The PlanetScale defaults that matter in production: safe migrations on main, deploy requests with a real revert plan, queries shaped for Vitess limits, and scoped credentials.

---
# PlanetScale Best Practices: Schema Changes, Limits and Connections

The PlanetScale practices that pay off in production are few: enable safe migrations on the production branch, ship every schema change as a deploy request, shape queries for the Vitess limits before they bite, and give each service its own scoped password. Most avoidable PlanetScale outages come from skipping one of those four.

> [!VERIFIED]
> PlanetScale Vitess safe migrations, deploy requests, column renames, system limits, Insights and connection strings, against the PlanetScale docs. Checked November 2026.

This page covers PlanetScale's Vitess (MySQL) product. If you are still deciding between PlanetScale and a serverless Postgres, start with [Neon vs PlanetScale](/blog/neon-vs-planetscale) and come back once the choice is made.

## Safe migrations on main is the first switch to flip

Safe migrations is a per-branch setting, and PlanetScale recommends it for every production branch. With it on, DDL sent straight to that branch is rejected, whether it comes from the console, a terminal or a MySQL GUI. Schema changes can only arrive through a deploy request, which means somebody reviews them.

```
$ pscale branch safe-migrations enable app main
$ pscale branch create app add-orders-status
$ pscale shell app add-orders-status   # ALTER TABLE orders ADD COLUMN status ...
$ pscale deploy-request create app add-orders-status
```

Under the hood, a deploy builds a ghost table with the new schema and keeps it in sync with production until cutover, so a large `ALTER TABLE` does not lock the table your app is writing to. Teams that leave safe migrations off "for speed" are trading that guarantee for the ability to run an unreviewed `ALTER` against production at 5 p.m. on a Friday.

## Treat the 30-minute revert as your rollback plan, not a safety net

After a deploy, PlanetScale keeps the old table in sync for 30 minutes so you can revert. After that window, the change is permanent. Three details decide whether the revert actually saves you:

- **Instant deployments cannot be reverted.** They also take an exclusive metadata lock and kill queries holding locks on the table, so your app needs retry logic. Use them only for changes you would never want to undo.
- **Reverts fail on data the old schema cannot hold.** Widen `VARCHAR(10)` to `VARCHAR(50)`, let users write 40-character values, and the revert has nowhere to put them.
- **Auto-apply is on by default.** Uncheck it for risky changes so the cutover waits for `pscale deploy-request apply` at a time you choose. An open gated deploy holds the branch's serial queue, so do not leave it parked overnight.

## Renames take two deploys, by design

PlanetScale does not support `RENAME` for tables or columns. The declarative diff cannot tell a rename from a drop plus an add, and it refuses to guess. The supported path is the same expand-and-contract sequence used on any busy database:

1. Add the new column (deploy request one).
2. Make the app write both columns, then backfill old rows.
3. Switch reads and writes to the new column.
4. Drop the old column (deploy request two).

It is slower than one `ALTER`, and it is the reason renames never take an outage. The same pattern for Postgres is in [zero-downtime migrations with expand and contract](/blog/zero-downtime-postgres-migrations-expand-contract).

## Design queries for the Vitess limits up front

The Vitess limits are not tuning knobs on the managed service. The ones that break real apps, as of November 2026:

| Limit | Value | What hits it |
|---|---|---|
| Rows returned, updated or deleted per query | 100,000 | Unpaginated exports, bulk `DELETE` |
| Result set size per query | 64 MiB | `SELECT *` on wide rows with JSON or text |
| Transaction timeout | 20 seconds from open | Long jobs inside one transaction, idle open transactions |
| Autocommit query timeout | 900 seconds | Heavy reporting queries |
| Connection lifetime | May be closed after 24 hours | Pools that never recycle connections |

**Where the limits bite:** a nightly cleanup like `DELETE FROM events WHERE created_at < ...` works for months, then the table grows past 100,000 matching rows and the job starts failing. Batch it by primary key from day one. The 20-second clock starts when the transaction opens, so a worker that opens a transaction, calls an external API, then commits will hit it even with fast queries.

**What to do instead:** paginate reads, batch writes in chunks of a few thousand rows, keep transactions to database work only, and set your pool's max connection lifetime below 24 hours. OLAP mode lifts the row limit for reads, not for updates or deletes, and is meant for occasional exports, not app traffic. Regular analytics belongs in a warehouse.

## Use Insights before you add capacity

Insights lists every query pattern with count, latency (p50 and p95 by default, p99 and p99.9 on toggle), rows read and rows returned. Queries running a full table scan carry a warning icon because they are not using an index. Before you resize a cluster, sort by rows read: a single unindexed lookup reading a million rows to return one is the usual cause of a slow database, and an index is cheaper than a bigger cluster.

## One password per service, per branch

Each PlanetScale password is scoped to one branch and cannot read other branches. Use that scoping deliberately:

- **One password per service and environment**, so revoking a leaked credential does not take down everything. Deleting a password disconnects its clients, which can take up to five minutes.
- **Restrict by IP** to your app servers or bastion. An unrestricted password accepts traffic from anywhere.
- **Use replica passwords for read-heavy paths.** A replica-type password routes queries to the branch's replicas and read-only regions, nearest first, so reporting and search reads stay off the primary.
- **Save it immediately.** Passwords are not stored in plaintext; a lost one gets replaced, not recovered.

## Questions people ask

### What are the most important PlanetScale best practices?

Enable safe migrations on every production branch so schema changes go through reviewed deploy requests. Use the 30-minute revert window deliberately and avoid instant deploys for anything you may undo. Paginate and batch to stay under the 100,000-row and 20-second transaction limits. Give each service its own branch-scoped, IP-restricted password.

### How do I rename a column in PlanetScale?

PlanetScale does not support `RENAME` for columns or tables. Add the new column in one deploy request, have the app write both columns, backfill existing rows, switch reads and writes to the new column, then drop the old one in a second deploy request. Plan for at least two deploys.

## The decision, concretely

- Production branch without safe migrations? Turn it on today; the only cost is losing ad-hoc DDL on main.
- Risky change to a hot table? Gated deploy, apply at a quiet hour, watch Insights for the full revert window.
- Job touching more than 100,000 rows? Batch by primary key; do not reach for OLAP mode in app code.
- Slow database? Check Insights for full table scans before you pay for a bigger cluster.

## The call we'd make

Start every PlanetScale database with safe migrations on main, a password per service, and batch-sized jobs. That covers most of the production surprises. The one practice teams skip and regret is watching the 30-minute revert window: treat each cutover as a short on-call shift, because after half an hour the only rollback left is another migration.

## Sources

- [PlanetScale docs: Safe migrations](https://planetscale.com/docs/vitess/schema-changes/safe-migrations)
- [PlanetScale docs: Deploy requests](https://planetscale.com/docs/vitess/schema-changes/deploy-requests)
- [PlanetScale docs: Handling table and column renames](https://planetscale.com/docs/vitess/schema-changes/handling-table-and-column-renames)
- [PlanetScale docs: System limits](https://planetscale.com/docs/vitess/planetscale-system-limits)

