The connection-count myth

A persistent belief holds that serverless compute inherently needs more database connections than traditional hosting. That's not accurate. The number of concurrent connections your application requires is determined solely by the number of concurrent requests you process — regardless of whether you run on a single large server, many small servers behind a load balancer, AWS Lambda, or an event-loop model with non-blocking I/O.

Consider 1,000 concurrent requests: you need 1,000 active pool clients. Traditional database connections are non-concurrent, so one pool client per in-flight request remains best practice across every compute model. Without pooling, unpredictable traffic exhausts connections in any environment. With pooling, the math is identical everywhere.

Model

Example setup

Pool math

Total pool clients

Single large server (threading)

1 server × 1,000 threads

1 pool × 1,000 clients

1,000

Multi-process

100 processes × 10 requests each

100 pools × 10 clients

1,000

Many small servers

100 servers × 10 requests each

100 pools × 10 clients

1,000

AWS Lambda

1,000 instances × 1 request each

1,000 pools × 1 client

1,000

Vercel Fluid

100 instances × 10 requests each

100 pools × 10 clients

1,000

If the steady-state math is the same, what's actually different about serverless?

The real issue: suspension, not scale

The genuine problem lies in the serverless VM lifecycle, which differs from serverful environments in one critical phase. A serverful machine proceeds through provisioning, serving traffic, and deprovisioning — the last step cleanly releasing all pool clients. A traditional serverless instance follows a different path:

  1. Provision or start up
  2. Serve traffic
  3. Suspend when idle — remain in memory but stop executing until another request arrives
  4. Eventually delete the suspended VM without a clean shutdown

Step 4 is not the serious problem. When the VM is deleted, its sockets to the database pool close and the pool reclaims those connections. Step 3 is where the trouble begins.

A pool client idle time should fire while the function is suspended, but it never does because the function is suspended. This “leaks” the pool client. A pool client idle time should fire while the function is suspended, but it never does because the function is suspended. This “leaks” the pool client. A pool client idle time should fire while the function is suspended, but it never does because the function is suspended. This “leaks” the pool client. A pool client idle time should fire while the function is suspended, but it never does because the function is suspended. This “leaks” the pool client.

Database connections exist in two states: active (executing a query) and idle (available for reuse). When a client becomes idle, a timer in the pool should eventually close it if no new request arrives. If the function is suspended, that timer never fires. The connection stays open until either the instance is shut down or the server-side pooler closes it via its own timeout — both typically taking minutes. During that window, the connection is effectively leaked.

When this actually matters

This only becomes a real concern when you're constrained on maximum connections at your database pooler. That constraint exists in practice: Supabase's free plan, for instance, caps concurrent pooler connections at 200. Leaking 50 of those can genuinely hurt.

The leaks can also be heavily correlated. Deploy a new version of your app, and 100% of your old serverless functions will suspend and never receive traffic again. All of them leak their connection pools until the pooler-side timeout kicks in.

In traditional serverless environments like AWS Lambda, there is no practical fix without trade-offs. Closing the database connection after every request stops leaks but introduces significant latency — every request would need to open a fresh connection.

A lifecycle-aware solution

Modern serverless platforms can address this directly. Vercel Fluid Compute supports waitUntil, which keeps a function alive just long enough to finish work after the main request completes. That window is sufficient to close idle connections before suspension.

The algorithm is straightforward:

  • When a client is released back to the pool, schedule a timer slightly longer than the idle timeout
  • Use waitUntil to keep the function alive until the timer fires and closes the client
  • If another client is released before the timer fires, cancel and reschedule

With this approach, the compute model closes pool clients exactly as a serverful solution would. In Fluid Compute, you don't implement this yourself — one line after pool configuration handles it:

import { Pool } from "pg";

import { attachDatabasePool } from "@vercel/functions"

const pool = new Pool({

connectionString: process.env.POSTGRES_URL,

});

attachDatabasePool(pool);

Cost impact

Keeping a function alive slightly longer does have a cost, but with Active CPU Pricing for Fluid Compute the impact is close to zero. While the function remains alive, it can still process new requests — only the very last request that would have leaked a connection adds extra time. Waiting for the idle timer consumes no CPU, so you pay only the small memory reservation cost.

Pool configuration best practices

Set low connection idle timeouts

You control your pool's idle timeout. If connections are limited, set it relatively low — around 5 seconds. That gives good reuse during busy periods while releasing idle connections quickly when traffic drops.

Define pools globally

In every compute model, define database connection pools at global scope so they're shared between requests. That's the core purpose of a pool and avoids unnecessary new connections.

Don't set max pool size to 1

Some advice recommends setting the maximum pool size to 1 for AWS Lambda. This doesn't reduce total connections — Lambda won't make more anyway — and it can still leak that single connection. For Fluid Compute and serverful models, a maximum of 1 actively prevents them from taking advantage of concurrent execution. The minimum pool size should typically stay at 1; increasing it is rarely useful except for extremely bursty workloads where the gap between bursts exceeds your idle timeout.

Use rolling releases

Vercel's rolling releases gradually shift traffic from one deployment to the next, avoiding a thundering herd of new services connecting to the database at once. Kubernetes rolling updates offer similar protection. If you're on Fluid Compute, also use attachDatabasePool to ensure idle connections close before suspension.

Bottom line

Serverless compute does not demand more database pool clients during normal operation. The real failure mode is leaked connections when functions suspend. Traditional serverless platforms have no clean solution. Modern platforms like Fluid Compute provide lifecycle hooks that release connections properly — with minimal cost and no latency impact.