Every serverless function that opens a Postgres connection pays 50-100ms for TCP setup, auth, and session initialization, then discards it. Multiply that across hundreds of concurrent invocations and you exhaust max_connections in seconds. Vercel’s Fluid Compute, enabled by default for all new projects since April 2025, keeps function instances warm across invocations. The attachDatabasePool API exploits that warmth to reuse a single open connection for many requests. Whether that actually replaces your external pooler depends on what else is hitting Postgres.
The serverless connection storm
Postgres was designed for persistent connections from a small number of application servers. Serverless inverts every assumption: hundreds of short-lived functions, each opening its own connection, each tearing it down when the invocation ends. The connection establishment cost alone runs 50-100ms per request when you account for the TCP handshake, authentication round-trip, and backend process allocation.
The standard fix has been an external pooler. PgBouncer, Supavisor, RDS Proxy, and Neon’s built-in pooler all sit between the function and Postgres, holding a small number of persistent backend connections and multiplexing client connections on top. This works until it doesn’t.
This is not a new problem. The 2muchcoffee Supavisor writeup documents the same failure shape: a constrained shared connection pool meeting mixed workloads where web apps, scheduled jobs, and Edge Functions all compete for the same Postgres connections.
What Fluid Compute does to your connections
Fluid Compute keeps a function instance alive between invocations, allowing it to handle multiple concurrent requests on Node.js and Python runtimes. Fluid Compute reduces cold-start frequency through automatic bytecode optimization and function pre-warming on production deployments, though cold starts are not fully eliminated. The pre-warming applies to production deployments; preview environments don’t produce the sustained traffic for Fluid’s instance-sharing to work efficiently.
The relevant API is attachDatabasePool, exported from @vercel/functions. It wraps database clients for PostgreSQL (pg), MySQL2, MariaDB, MongoDB, Redis (ioredis), and Cassandra. Instead of creating a new connection per invocation, the function reuses the connection already open on the warm instance.
import { attachDatabasePool } from '@vercel/functions';import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });attachDatabasePool(pool);The pricing model reinforces this: Fluid Compute charges for Active CPU time, meaning you pay only while the function is processing, not while it’s waiting on a database query or an AI model call. A connection that stays open across invocations costs nothing during the I/O wait.
When you might not need PgBouncer
For a single-workload app hitting one Postgres instance through Supabase or Neon, attachDatabasePool may be sufficient. If your function handles a few dozen requests per second on a warm instance, the single open connection serves them sequentially. No external pooler, no PgBouncer transaction mode, no connection churn.
The math is straightforward. One Fluid instance, one open Postgres backend connection, many sequential requests. For small apps, that’s enough. The connection establishment overhead drops to near-zero after the first invocation on a given instance.
The idle-eviction blind spot
Vercel has not published idle-eviction timers, maximum instance lifetimes, or connection-reuse duration numbers for Fluid Compute. This is the gap. If a Fluid instance sits idle for 30 seconds and gets evicted, the next invocation creates a new instance and a new connection. If it stays warm for five minutes, the savings compound.
This matters because the economic argument for attachDatabasePool over an external pooler depends on how often instances actually stay warm. On a low-traffic app with bursts separated by minutes of silence, eviction rates could be high enough that the connection-reuse savings are marginal. On a sustained-traffic app, instances stay hot and the savings are real.
Two pooler failures in production
Two teams recently documented production failures with external poolers under Fluid Compute, which illustrates both the problem and the limitations of existing solutions.
Circleback migrated from PgBouncer to PgDog because PgBouncer’s single-threaded architecture could not assign connections fast enough when hundreds of serverless functions spun up simultaneously during deployments. The connection spikes persisted for minutes after the deploy finished. PgBouncer’s design is well-suited to a handful of long-lived app servers, not to serverless fan-out.
2muchcoffee documented a different failure mode with Supabase’s Supavisor: under Fluid Compute, client connections and Postgres backend connections diverge sharply. New Node processes spawn and open client connections, while actual Postgres backends stay steady. The connection count on the client side balloons while the backend count looks normal, creating a failure shape most teams miss until the first gateway timeout cascade.
Why the bulkhead pattern still matters
attachDatabasePool gives you connection reuse within a single Fluid instance. It does not give you workload isolation. If your web app, a background worker, and pg_cron jobs all hit the same Postgres instance through Fluid, they compete for the same backend connections. A slow cron job does not care that your API function has a warm connection. It will block on its own query, holding a backend connection your API needs.
PgBouncer in transaction mode, PgDog’s multi-threaded architecture, and Supavisor’s connection multiplexing all attempt to solve this by inserting a coordination layer between clients and Postgres. Each introduces trade-offs. PgBouncer is single-threaded. Supavisor lacks workload isolation. PgDog addresses PgBouncer’s throughput ceiling but is a newer project with less production exposure.
Fluid’s connection reuse is a single-instance optimisation. If you need horizontal pool management, multi-tenant isolation, or bulkheading between workloads, you still need a pooler.
Decision matrix
| Scenario | Recommended approach | Why |
|---|---|---|
| Single web app, one Postgres, low-to-moderate traffic | attachDatabasePool only | Instance reuse handles connection lifetime; no external pooler overhead needed |
| Web app + background workers on same Postgres | attachDatabasePool + external pooler in transaction mode | Workers need bulkhead isolation from web traffic |
pg_cron or scheduled jobs hitting same DB | External pooler with per-tenant limits | Supavisor’s shared backend pool can be starved by cron jobs |
| Deploy-time connection spikes (many functions spinning up) | PgDog or multi-threaded pooler | PgBouncer’s single-threaded bottleneck cannot assign connections fast enough |
| Preview/CI environments | External pooler | Preview environments don’t produce sustained traffic for Fluid’s instance-sharing to work efficiently |
The honest summary: attachDatabasePool removes the pooler as a requirement for simple single-workload apps on Vercel. It does not remove the pooler as a requirement for any architecture where multiple workload classes share a Postgres instance. The idle-eviction numbers Vercel has not published would determine whether the economics hold under bursty traffic patterns. Until those numbers exist, the claim that Fluid replaces external poolers is structurally incomplete.
Frequently Asked Questions
Why does connection pooling break in every compute generation?
The same failure shape recurred across five generations: CICS, TP Monitor, JDBC pools, RDS Proxy, and now Supavisor/Hyperdrive. The root cause is structural. The relational database protocol bundles TCP socket state, session state, and transaction context into a single network primitive, so no compute model can separate “I need a socket” from “I need a transaction” without inserting an intermediary that reintroduces the pooling problem it was meant to solve.
What does Fluid’s bytecode caching store, and does it work in preview?
Fluid persists V8 compiled bytecode across invocations on the same instance, skipping the parse and compile phase on subsequent requests. This only applies to production deployments. Development and preview environments recompile from scratch every invocation, which is one reason connection reuse is less effective outside production.
What incompatibilities do PgBouncer and Supavisor introduce beyond throughput limits?
PgBouncer in transaction mode conflicts with prepared statements, forcing application-level workarounds or disabling server-side prepared statements entirely. Supavisor routes requests through an opaque five-hop path between client and Postgres backend, making latency diagnosis difficult when gateway timeouts cascade. Each pooler trades compatibility or observability for multiplexing.
What Fluid mechanisms besides attachDatabasePool affect cold-start frequency?
Fluid combines five strategies: request concurrency, bytecode caching, waitUntil (extending function lifetime past the HTTP response), Scale to One (retaining the last warm instance during traffic drops), and predictive scaling based on traffic patterns. Vercel reports these collectively eliminate cold starts for 99.37% of requests on sustained production workloads. That figure does not cover deploy-time spikes or bursty preview traffic.