Why Supabase & Neon Freeze at 100 Concurrent Users
A plain-English breakdown of connection exhaustion, unindexed foreign keys, and how connection pooling prevents 500 errors when traffic surges.
One of the most common inflection points for a growing application occurs right around 100 to 500 concurrent users.
During local testing and early beta demos, database queries return in a crisp 15 milliseconds. But as soon as a launch announcement goes live on Product Hunt or Twitter, the application suddenly throws cryptic errors:
Error: remaining connection slots are reserved for non-replication superuser connections
FATAL: too many connections for role "postgres"
Here is a plain-English explanation of why database concurrency bottlenecks happen, and the two architectural steps required to handle high traffic without breaking a sweat.
The Cause: Direct Connections vs. Serverless Scale
In modern serverless architectures (like Next.js on Vercel, Astro, or Cloudflare Workers), every incoming user request can spawn an ephemeral serverless execution context.
If your code opens a direct TCP connection to Postgres for every request:
- 100 simultaneous users can trigger 100 separate database connections.
- Default Postgres instances typically cap maximum direct connections between 60 and 100 to protect server memory.
- Once that limit is reached, Postgres simply refuses new connections, returning instant 500 errors to your customers.
Solution 1: Implement PgBouncer Connection Pooling
Instead of allowing every serverless function to open a private connection directly to Postgres, we place a lightweight connection pooler (like PgBouncer or Supabase’s built-in transaction pooler on port 6543) in front of the database.
A connection pooler maintains a small, fixed pool of open database connections (e.g. 15 connections) and rapidly shares them across thousands of incoming requests in milliseconds.
// ❌ Direct connection: Exhausts connections at ~80 concurrent users
DATABASE_URL="postgres://postgres:password@db.project.supabase.co:5432/postgres"
// ✅ Transaction Pooled connection: Handles 5,000+ concurrent requests effortlessly
DATABASE_URL="postgres://postgres:password@db.project.supabase.co:6543/postgres?pgbouncer=true"
Solution 2: Index Missing Foreign Keys
When tables are created rapidly during MVP prototyping, relational foreign keys (like organization_id on an invoices table) often lack database indexes.
Without an index, Postgres must perform a Sequential Table Scan (reading every single row on disk) every time it fetches a user’s records. Adding single-line B-tree indexes reduces database CPU usage by up to 95%:
-- Dramatically accelerates user queries under load
CREATE INDEX CONCURRENTLY idx_invoices_org_id ON invoices(organization_id);
Need Help Scaling Your Data Layer?
If your application is preparing for a traffic milestone or experiencing database latency, our engineers can audit your queries, configure connection pooling, and verify concurrency limits. Contact our team to schedule a review.
Have our team inspect your AI-generated codebase.
48-hour turnaround with direct pull requests fixing security and scale bottlenecks.