PlanetScale, or a database treated like code
Changing a production database schema is one of the few operations where experienced developers still hold their breath. There are no branches, no change review, no one command rollback. There is a statement that either goes through or locks a table for twenty minutes in the middle of the day.
PlanetScale was built around the thesis that a database should work like a code repository. You create a branch, make the change there, submit it for review, and after approval the system applies it in production without locking the table. If something goes wrong, you roll back with one click.
Two engines under one shell
The platform began as a MySQL service and was known for that. Since 2025 a Postgres variant is available too, and that changes the picture.
The MySQL variant sits on a query distribution layer known from very large deployments. It offers splitting data into shards and horizontal scaling, at the cost of several limitations covered below.
The Postgres variant came later, with a different architecture based on consensus between nodes and local high performance disks. There is no distribution layer there, so none of its limitations either.
What both share is the working model: branches, deploy requests, and change review behave the same on either variant. That means choosing an engine reduces to ordinary questions of application compatibility rather than giving up the reason you came here.
A practical hint for a new project: unless something ties you to MySQL, Postgres is more convenient here, since it imposes none of the distribution layer's limits and starts cheaper.
Database branches in practice
This is the feature people come here for, so it deserves seeing in action.
pscale branch create shop add-discounts
pscale shell shop add-discountsA branch is created as a copy of the production schema, without copying all the data. It appears in seconds, so every developer can have their own, and every code branch its own database branch.
ALTER TABLE orders ADD COLUMN discount DECIMAL(5,2) DEFAULT 0;
CREATE INDEX idx_discount ON orders (discount);Once the changes are in, you submit them for deployment.
pscale deploy-request create shop add-discountsThe request shows the schema difference in human readable form and warns about risky operations. After approval the change goes to production in the background, through a replication mechanism, and the cutover happens only once the new structure is ready. The table is not locked, and a rollback is available if something goes wrong.
That last part deserves emphasis, since in the classic arrangement rolling back a migration means writing a reverse migration and praying the data has not drifted irreversibly in the meantime.
What the MySQL variant cannot do
This section is the most important in the whole text, since it covers things usually discovered three weeks into the work.
Foreign key constraints behave differently from classic MySQL and for years were simply unavailable. The reason is architectural: with data spread across many nodes, enforcing a relation between them is expensive. Support has improved, but before deciding, check that your schema genuinely works in this arrangement.
The practical consequence is that referential integrity is guarded by the application or the data access layer. Tools such as Prisma or Drizzle can emulate that behaviour, so the code looks familiar, while the database will not stop a write violating a relation.
The second matter is queries joining many tables across sharded data. They work, but may require pulling data from several nodes, which can be expensive. When designing a schema, keep together the data you query together.
The third is long transactions. The environment is tuned for short operations, so a transaction held open for minutes is an antipattern here.
Most of these limits do not apply to the Postgres variant, since there is no distribution layer. That is one reason new projects more often pick that engine today.
Branches in a team workflow
The ability to create branches gives little until it is wired into how the team works, and that part you must think through yourself.
The simplest working arrangement looks like this. A database branch is created alongside a code branch and carries the same name, so the pairing is obvious to everyone. The schema deploy request is opened alongside the code change request and goes through the same review. Schema deployment happens before code deployment, since a new column must exist before the application starts writing to it.
That ordering carries a consequence easy to forget. A change removing a column must run in reverse: first the code stops using it, then the column disappears. Done in the wrong order it produces a few minutes of production errors, and that is the most common cause of a failed deployment in this model.
Base the test environment on a separate long lived branch rather than on production. Final testing then runs against the structure about to reach production rather than the one already there.
Tie branch cleanup to closing the request in the code repository. Manual deletion always loses to team memory, and unused branches linger for months while accruing cost.
Pricing and the billing model
The free tier was withdrawn in 2024 and has not returned. That change matters, since plenty of material online still describes a free version that no longer exists, while a project here starts on a paid plan.
The lowest plan starts at a few dollars a month for a single Postgres node. The MySQL variant has no single node equivalent and comes only in the failure resistant arrangement, meaning three nodes, so its cheapest cluster costs tens of dollars a month rather than a few. A separate family of plans runs on dedicated hardware with low latency disks, starting at fifty dollars a month and aimed at workloads sensitive to response time.
Billing follows node resources rather than query count, which has two consequences. The first is pleasant: the bill does not jump with the number of queries. The second less so: you pay while the database sits idle too, so for a test environment used once a week this is not the cheapest choice.
The node is not the whole bill, though, and that is where the usual surprise hides. Storage beyond the ten gigabytes included with network attached disks, backups, and egress beyond the included allowance all bill separately; the pricing calculator shows that allowance as a hundred gigabytes a month, with six cents per gigabyte above it. An application shipping a lot of data outward therefore carries a line that grows directly with traffic, even though the plan itself is flat.
Development branches are billed for the time they exist, prorated to the millisecond. A branch forgotten for a month costs for that whole month, which is why cleanup deserves automating rather than being left to team memory.
Prices change more often than documentation, so check the current price list on the service's site before budgeting for a year. Price the side environments too, since with three environments and several branches the bill looks different from a single production database.
Connections and working with an application
The connection layer looks ordinary here, while two things differ enough to be worth knowing before the first deployment.
The first is connection encryption, which is mandatory. A client without it simply will not establish a session, and the error message is often misleading, pointing at authentication rather than the cause.
const connection = {
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: { rejectUnauthorized: true }
}The second is the number of concurrent connections under serverless deployment. Every function instance opens its own, so a traffic spike can exhaust the limit within seconds. There are two answers: a pooler managing connections, or a driver communicating over a network protocol rather than a classic socket.
That second variant deserves attention particularly in edge environments, where a classic database connection is unavailable altogether. The query then travels as an ordinary network request, which adds slight overhead while opening the option of running logic close to the user.
Issue credentials separately per environment and per branch rather than sharing one set. Revoking access for one environment without disturbing the others is then a single click rather than a password change in five places at once.
Performance and query insight
A database stops being the bottleneck quickly once you can see which queries cost the most, and that insight is often the weakest part of self managed deployments.
The built in view shows queries grouped by shape, together with call counts and execution times. It is on by default, so nothing needs configuring to discover that one query runs forty thousand times a minute.
The most common discovery on first inspection is a query inside a loop, arising accidentally from lazy relation loading in the data access layer. It looks innocent in code and produces one query per row of a list. The fix usually means joining the relation explicitly in a single query.
The second is a missing index on a column used for filtering. At a thousand rows nobody notices, at a million the difference reaches seconds. Check the execution plan for your most frequent queries before the data grows.
The third is selecting every column where two are needed. On wide tables with text fields that can be an order of magnitude difference in transferred data, and the fix fits on one line.
Make a habit of looking there after every larger deployment rather than only once the site starts crawling. A new query running a hundred times more often than its author assumed shows up in that listing immediately, while a week later it disappears among the rest of the traffic and takes considerably longer to find.
PlanetScale against the alternatives
| Option | Strength | Weakness | Pick it when |
|---|---|---|---|
| PlanetScale | Branches, zero downtime deploys, scale | No free tier, MySQL limitations | A team changing schemas often |
| Neon | Postgres with branches, suspend when idle | Cold start after suspension | Test environments and side projects |
| Supabase | Database plus auth, files, and an API | More moving parts to handle | An application needing a whole backend |
| A cloud provider's database | Full control, maturity | Migrations and maintenance are yours | A team with a database administrator |
Choosing between the first two rows comes down to the billing model. Suspending a database when traffic stops is a huge advantage for side environments and a drawback for a service that must answer instantly at three in the morning.
The third row is a different category: not a database alone but a set of services around it. If you need only a database, adding the rest is surplus.
Common mistakes
The first is planning a project around the free tier. It was withdrawn, so cost must be accounted for from day one.
The second is choosing the MySQL variant without checking whether the schema copes with the distribution layer's limits. On a new project with no legacy, Postgres is usually the simpler choice.
The third is assuming the database will enforce referential integrity. If the data access layer does it rather than the database, a write bypassing that layer will go through unchallenged.
The fourth is changing a schema without a deploy request. It is possible, and then you lose review, background deployment, and rollback, meaning everything you came here for.
The fifth is forgetting to delete branches. They appear in a second and stay for months, and each one costs something.
The sixth is porting long transactions verbatim from a classic deployment. The environment is tuned for short operations and expects them.
FAQ
Does PlanetScale have a free plan?
No. The free tier was withdrawn in 2024 and has not been restored, despite plenty of online material still mentioning it. The lowest plan is a few dollars a month for a single node, and current rates are worth checking on the price list.
MySQL or Postgres?
On a new project with no legacy, usually Postgres, since it imposes none of the distribution layer's limits and starts cheaper. Choose MySQL when the application already runs on it or when you need sharding at very large scale.
Does a database branch copy the data?
No, a branch is created as a copy of the schema alone, which is why it appears within seconds regardless of database size. Production data can be brought into a branch separately if testing requires it.
Does it work with popular data access layers?
Yes, connections use the standard protocol, so Prisma, Drizzle, and other tools work normally. On the MySQL variant, enable relation emulation if your data access layer offers it.
How does it differ from a cloud provider's database?
In the working model for schema changes. A cloud provider gives you a machine with a database and you run migrations yourself. Here you get branches, change review, and background deployment, which for a team changing schemas often is a real difference in pace.
Documentation sits on the service's site, and the layer underneath the MySQL variant is described by the Vitess project.