How to Fix PostgreSQL "FATAL: Sorry, Too Many Clients Already" With Serverless Next.js
Quick answer
Your Next.js app deployed on a serverless platform (Vercel, AWS Lambda, Netlify Functions) works fine at first, then starts throwing connection errors under...
Your Next.js app deployed on a serverless platform (Vercel, AWS Lambda, Netlify Functions) works fine at first, then starts throwing connection errors under any real traffic. This is a fundamental mismatch between how serverless functions scale and how PostgreSQL handles connections β each function invocation can open its own new connection, and PostgreSQL's connection limit gets exhausted almost immediately under concurrent load.
The Problem
API routes that worked fine during development start failing once deployed and receiving real traffic:
Error: FATAL: sorry, too many clients already
at Parser.parseErrorMessage (/var/task/node_modules/pg-protocol/dist/parser.js:283:98)
Checking PostgreSQL directly during a traffic spike confirms the connection count maxing out:
SELECT count(*) FROM pg_stat_activity;
count
-------
100
Why It Happens
Traditional server applications hold a small, stable pool of database connections for the lifetime of the process. Serverless functions work fundamentally differently β each invocation may run in a fresh execution environment, and depending on your database client setup, each one can open a brand-new PostgreSQL connection rather than reusing an existing pool. Under concurrent traffic, a burst of 50 or 100 simultaneous function invocations can mean 50 or 100 simultaneous new connections, quickly exceeding PostgreSQL's max_connections limit (often 100 by default, and lower still on smaller managed database tiers). This gets worse because:
- Connection pooling libraries that work great in a long-running server process (like a standard connection pool in Express) don't behave the same way across separate, short-lived serverless invocations β each cold start effectively resets the pool.
- Serverless platforms can scale extremely quickly under traffic spikes, multiplying the connection count far faster than a traditional autoscaling server fleet would.
- Database clients not explicitly closing connections at the end of each function invocation leave connections open and idle, consuming slots without doing any useful work.
The Fix
The standard, most effective fix is putting a connection pooler like PgBouncer between your serverless functions and PostgreSQL, so the database itself only ever sees a small, stable number of actual connections regardless of how many function invocations are happening concurrently:
# pgbouncer.ini
[databases]
mydb = host=your-db-host port=5432 dbname=mydb
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
Point your application's connection string at PgBouncer instead of directly at PostgreSQL:
DATABASE_URL=postgres://user:pass@pgbouncer-host:6432/mydb
If you're on a managed platform, use their built-in pooling rather than running your own PgBouncer instance β Supabase, Neon, and AWS RDS Proxy all offer this as a managed feature specifically designed for exactly this serverless connection-storm problem:
# Example: Supabase's pooled connection string (port 6543 for pooled, not 5432 direct)
DATABASE_URL=postgres://user:pass@db.supabase.co:6543/postgres?pgbouncer=true
Within your application code, make sure each function invocation properly closes or releases its connection rather than leaking it, and consider caching the connection pool instance across invocations within the same execution environment where the runtime allows it:
// Cache the pool outside the handler so warm invocations reuse it
let pool;
function getPool() {
if (!pool) {
pool = new Pool({connectionString: process.env.DATABASE_URL, max: 1});
}
return pool;
}
export default async function handler(req, res) {
const client = await getPool().connect();
try {
const result = await client.query('SELECT * FROM users');
res.json(result.rows);
} finally {
client.release();
}
}
For edge runtime environments where a traditional TCP connection pool isn't practical at all, consider an HTTP-based database driver specifically designed for serverless and edge use, which avoids holding persistent connections entirely:
import {neon} from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);
const users = await sql`SELECT * FROM users`;
Still Not Working?
If you've added pooling but still see occasional spikes hitting the limit, check whether your pool's max_client_conn and default_pool_size settings are actually sized appropriately for your real traffic patterns, and monitor connection counts directly during a load test rather than assuming the pooler alone eliminates the problem entirely:
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
A large number of connections in idle in transaction state specifically suggests your application code isn't properly committing or rolling back transactions before releasing connections back to the pool, which prevents the pooler from actually reusing them efficiently even though pooling is nominally in place.