T
he pattern is predictable. You pick a managed database platform because you want to ship, not configure Postgres. It works. The product gets traction. Then, somewhere between Series A and the first real traffic spike, the database becomes the thing you're managing most.Connection limits. Unexpected query latency. A pricing page that made sense at $0 and doesn't at $2,000/month. The lock-in you didn't notice until you needed to leave.
This post is about when Neon, Supabase, and PlanetScale stop being the right choice and what the migration actually looks like. If you're evaluating these platforms before you've built anything, it's also a guide to what you're signing up for.
What each one actually is
Before the tradeoffs, get clear on what you're comparing. These are not the same category of product.
Neon is serverless PostgreSQL. The selling point is that compute scales to zero when your database isn't being queried, which makes it nearly free for development and staging environments. Neon is a database. That's it. Connection pooling via PgBouncer is built in. Branching creating isolated database copies for dev or testing is the feature that genuinely has no equivalent elsewhere.
Supabase is a Firebase alternative built on Postgres. You get a database, but you also get auth, object storage, edge functions, and realtime subscriptions. That breadth is both the appeal and the risk. Teams often start using Supabase's auth and storage alongside the database, then discover those layers are the ones that make migration painful not the database itself.
PlanetScale is managed MySQL built on Vitess, the same technology that powers YouTube's database infrastructure. The key feature is horizontal sharding without application-level changes, which matters at genuinely large scale. PlanetScale ended their free tier in early 2024 and shifted focus to larger paid customers. If you're evaluating it now, you're looking at a paid product from day one.
Understanding what each one is clarifies which limits you'll hit first.
When managed databases stop making sense
The ceiling is different for each platform, but the categories of friction are the same.
Connection limits. Serverless Postgres has a connection problem. Each connection to Postgres is a heavyweight OS process. Neon's free tier caps you around 100 connections. Supabase's free tier is around 60. In a traditional server application with a single connection pool this is fine. In a modern architecture with multiple services, serverless functions, or background workers each opening their own pool, you burn through that headroom fast.
The standard mitigation is PgBouncer in transaction pooling mode. Neon includes it. Supabase includes it via their pooler endpoint (port 6543, not 5432 easy to miss in the docs). But "included" and "correctly configured" are different things. Many teams are hitting connection errors in production because they're connected to the direct Postgres port and their ORM is opening 10 connections per instance across 20 Lambda functions.
Query performance tuning. Managed Postgres abstracts the database configuration. That's convenient until you need to tune it. autovacuum settings, work_mem, max_parallel_workers you don't control these on Neon or Supabase's lower tiers. When a slow query is slow because of a planner decision influenced by stale statistics, your options are limited to adding an index or rewriting the query. Sometimes that's enough. Sometimes it isn't.
Cost at scale. Neon's compute-hours pricing model is genuinely cheap for low-traffic workloads where the database sleeps between requests. At constant load it's comparable to or more expensive than a dedicated RDS instance. Supabase's Pro plan at $25/month is reasonable until your database grows past 8GB, at which point you're paying $0.125/GB/month for storage and Supabase counts logical replication WAL data toward that limit, which surprises teams running read replicas or Debezium-based CDC.
The Supabase lock-in calculation. This one deserves direct attention. If you're using Supabase only as a Postgres database, migrating is a pg_dump and a DNS change. If you're using Supabase Auth, the user table structure and JWT configuration are Supabase-specific. If you're using Supabase Storage, your files are in their S3-compatible layer and your file paths reference Supabase URLs. Migrating all three simultaneously while keeping the product live is a significant engineering project. Teams that use Supabase as a convenience at the start often treat it as infrastructure by the time they need to leave.
[→ Read: RDBMS vs NoSQL The Decision That Haunts Startups Three Years Later] covers the broader database selection problem. The same principle applies here: the choice that's easiest at zero scale is often the one you're rewriting around at real scale.
What you're paying for that you might not need
Supabase charges for the full platform whether you use all of it or not. If you're using Supabase as a plain Postgres host with no auth, no storage, no realtime you're paying for a feature set that AWS RDS or a self-hosted Postgres instance would give you more cheaply with more control.
PlanetScale charges for reads and writes separately, which sounds straightforward until you have a high-read application with aggressive caching. Their row reads pricing can make queries that would be cheap on a flat-rate instance surprisingly expensive. Audit your query patterns before committing.
Neon's branching feature is genuinely useful for teams running database migrations in CI. Creating a branch of production data to test a schema migration against is a capability most teams build custom tooling to approximate. If your team runs regular migrations and values pre-production schema validation, that alone can justify staying on Neon longer than the cost model suggests.
The honest question is: what are you actually using? If the answer is "just Postgres," self-managed options are cheaper and more flexible at most scales that matter.
The part most people get wrong
The most common mistake is connecting to the wrong endpoint and not knowing it.
Supabase gives you two connection strings: the direct Postgres port (5432) and the pooler port (6543). The direct port bypasses PgBouncer. Every persistent connection from your application goes directly to Postgres. With a single server application maintaining a pool of 20 connections, this is fine. With Next.js API routes running as serverless functions where each invocation may open a fresh connection it is not fine. You'll hit connection limit errors under load, often in production, often during a traffic spike.
The fix is one line: change your DATABASE_URL to use port 6543 and add ?pgbouncer=true if your ORM requires it (Prisma does). But teams frequently set this up in development with a direct connection, deploy to production with the same string, and don't hit the limit until real traffic arrives.
The same applies to Neon. Their pooler is available and documented, but it's not the default connection string in the dashboard. You have to select it explicitly.
For PlanetScale, the equivalent gotcha is foreign key constraints. Vitess doesn't enforce them. If your schema relies on database-level FK enforcement and most Rails and Laravel apps do by default you'll get no errors when you write orphaned records, and you'll discover the data integrity problem later, not during the write. PlanetScale's documentation explains this clearly. Many teams don't read it before they're in production.
Separately: if your architecture is evolving toward multiple services, each hitting the same database, you're building toward the scaling wall that [→ database sharding] addresses. Managed platforms make sharding invisible. When you need to move past them, the sharding conversation becomes real.
Real-world example
A marketplace startup I worked with had their entire stack on Supabase: Postgres database, auth, and file storage for user-uploaded images. At around 30,000 monthly active users their Pro plan cost was $95/month $25 base plus storage and compute overages.
Three things happened at the same time: storage costs spiked as users uploaded more files, they started hitting connection limit warnings during peak hours, and the engineering team wanted to add a Go-based background worker that needed direct database access with fine-grained connection pool control.
We broke the migration into three independent phases. First: file storage. We moved user uploads from Supabase Storage to S3 with a CDN in front of it. User-facing URLs were rewritten transparently. No application downtime, two days of engineering work.
Second: auth. We migrated to a self-hosted Auth.js setup against their own Postgres instance. This required a user migration script and a short maintenance window. Existing sessions were invalidated users had to log in once. We communicated this in advance and did it on a Sunday morning.
Third: database. With auth and storage gone, the Supabase dependency was a plain Postgres database. pg_dump, restore to RDS in ap-southeast-3 (Jakarta, for their Indonesian user base), update the connection string, done.
Total migration time: six weeks elapsed, around three weeks of actual engineering work spread across the team. Monthly infrastructure cost dropped from $95 to $38. More importantly, the engineering team could now tune the database configuration directly and add read replicas without waiting for a support ticket.
FAQ
Q: Is Supabase a good choice for a new startup? A: For early-stage products where shipping fast matters more than infrastructure control, yes. The free tier is generous, the DX is good, and the built-in auth saves significant time. The risk is how deeply you integrate their non-database features. Use Supabase Postgres freely. Use Supabase Auth carefully understand the migration cost before you build your user model around it.
Q: When should I move off Neon to a dedicated Postgres instance? A: Watch for three signals: you're regularly hitting connection limits despite using the pooler endpoint, your monthly Neon bill is approaching or exceeding what equivalent RDS or self-hosted Postgres would cost, or you need configuration control over the database server that Neon doesn't expose. The branching feature is genuinely hard to replace, so factor in whether your team actively uses it.
Q: Does PlanetScale's lack of foreign key support matter for my app? A: It depends on how you handle data integrity. If your application layer enforces all referential integrity your ORM validates relationships before writing you're fine. If you rely on the database to reject orphaned records or cascading deletes, you need to move that logic to the application or choose a different database. Auditing your schema for FK dependencies before migrating to PlanetScale is not optional.
Q: What's the simplest path to self-hosted Postgres when you're ready to leave Supabase?
A: If you're only using Supabase as a database (no auth, no storage), it's a pg_dump and a connection string change. RDS Postgres in the region nearest your users, or a managed Postgres on DigitalOcean or Hetzner, gives you the same Postgres with more control and typically lower cost at medium data volumes. The migration window can be as short as an hour with proper preparation.
Q: What do teams typically replace Supabase Auth with? A: Auth.js (formerly NextAuth) for Next.js applications is the most common replacement. Clerk is popular for teams that want a managed auth service with similar DX to Supabase but without the database dependency. Self-hosted Keycloak for teams with enterprise auth requirements. The right answer depends on whether you want to manage auth infrastructure or pay someone else to manage it just without the database coupling.
These platforms exist because they solve a real problem: Postgres is operationally heavy at zero scale, and teams building products shouldn't be spending their first months configuring databases. They're good tools used incorrectly when teams treat them as permanent infrastructure rather than scaffolding.
Know what you're using each feature for. Know the thresholds. Migrate before the platform is blocking you, not after.