Why Serverless Functions Keep Exhausting Your Postgres Connections
September 21, 2026
4 min read
Serverless functions open far more Postgres connections than a traditional server ever would, and raising max_connections only delays the failure. Here's why connection pooling is the actual fix, and what it costs you.
Ayodele Miracle JohnSOFTWARE ENGINEER
The Problem
Serverless functions scale by running many short-lived instances in parallel, and each instance that touches Postgres wants its own database connection. That's fine at low concurrency. It falls apart the moment traffic spikes, because Postgres doesn't scale connections the way it scales queries.
A default Postgres install caps out at 100 connections, and managed databases often set the limit lower once you account for connections reserved for replication, monitoring, and admin access. A burst of 300 concurrent Lambda or Vercel function invocations, each opening its own connection, will blow past that limit in seconds. The failure mode isn't a slow query. It's a flat "too many connections" error, and it usually shows up first in production, under load, not in a staging environment running at a tenth of the traffic.
Why More Connections Isn't the Answer
The instinct is to raise max_connections. That works until it doesn't. Every open Postgres connection holds its own backend process and memory footprint (parsed query plans, temp buffers, session state) whether or not it's doing anything. Push the limit high enough to absorb serverless concurrency and you're now paying for idle memory most of the time, with a database that becomes fragile exactly when it's under the load you sized for.
The deeper issue is architectural. Traditional connection pooling, a pool held in application memory and reused across requests, assumes a long-lived process. Serverless functions are the opposite: short-lived, frequently cold-started, with no shared memory between invocations. Each instance either opens a fresh connection or, worse, leaks one that never gets cleaned up when the instance is recycled.
How an External Pooler Fixes It
The fix is to stop pooling in the application and pool in front of the database instead. A connection pooler, such as PgBouncer, Supabase's Supavisor, or a managed equivalent like AWS RDS Proxy, sits between your functions and Postgres as its own persistent process. It accepts a large number of client connections and multiplexes them onto a small, fixed set of backend connections to the database.
That gives you two independent limits to reason about instead of one: how many clients can connect to the pooler, and how many backend connections the pooler holds open to Postgres. The pooler absorbs the serverless side's churn, connections opening and closing constantly, without that churn ever reaching the database itself.
What You Lose in Transaction Mode
Most poolers default serverless deployments to transaction-mode pooling, where a backend connection is only checked out for the duration of a single transaction and returned to the pool immediately after. It's the mode that makes pooling actually work under high concurrency, but it isn't free.
Because a client's next transaction can land on a completely different backend connection, anything that depends on session state stops working reliably: prepared statements, SET-based session variables, LISTEN/NOTIFY, advisory locks, and temp tables all assume a stable connection across statements. If your ORM prepares statements by default, and several do, you'll either need to disable that behavior or confirm your pooler and driver combination handles it (some, including recent Supavisor and PgBouncer versions, support limited prepared-statement compatibility in transaction mode). This is the detail that turns "add a pooler" from a one-line config change into something worth testing under real load before it ships.
Picking a Pooler and Sizing It
Which pooler makes sense depends on where you're already hosted. If you're on Supabase, Supavisor is the built-in option and requires no extra infrastructure. On AWS, RDS Proxy integrates directly with Lambda and IAM auth. Running your own Postgres, PgBouncer remains the standard, self-hosted choice. Prisma users have Prisma Accelerate as a managed alternative that adds caching on top of pooling.
However you deploy it, don't size the backend pool by guessing. A widely used starting formula is (CPU cores × 2) + effective disks, which reflects that Postgres throughput is bounded by how much work the database server can actually parallelize, not by how many clients are waiting. Oversizing the pool doesn't add capacity, it just adds contention. Start conservative, watch pool wait time in your metrics, and size up only when wait time, not connection count, tells you to.

Sep 20, 2026
5 min read
Idempotency Keys: Making Payment APIs Safe to Retry
How idempotency keys stop a flaky network from turning one API request into two duplicate charges, and the practical details of implementing them correctly.

Sep 14, 2026
4 min read
What the Model Context Protocol Actually Is (and Why It Became the Standard)
MCP is mentioned in almost every AI engineering conversation right now but rarely explained plainly. Here's what the protocol actually does, how it works under the hood, and why nearly every major AI vendor ended up adopting it.

Notes
Sep 4, 2026
3 min read
What Google Earth Engine Actually Is (and What It's Capable Of)
Google Earth Engine gets mentioned a lot but rarely explained simply. Here is what it actually is, what it can do, and what changed in 2026, written for people with no background in satellites or geospatial data.