If your serverless app has started throwing FATAL: too many connections for role after a deploy or a traffic spike, the first action is to look at the connection string your functions use. If it points at the database host directly, every function instance opens its own Postgres connection, and that is the problem to fix. On serverless platforms such as Vercel functions, AWS Lambda or Supabase Edge Functions, a pooler between your code and the database is what keeps the count under control.
Why does serverless Postgres run out of connections so fast?

Serverless Postgres runs out of connections because each function instance can open its own connection, and Postgres starts a dedicated backend process for every one of them. The default max_connections is typically 100, so a burst of concurrent invocations hits the ceiling quickly.
Both numbers come from the PostgreSQL documentation. PostgreSQL implements a "process per user" model, where each client connection gets exactly one backend process, and the max_connections setting defaults to "typically 100 connections" unless your kernel settings allow fewer. A long-running server opens a few connections and reuses them. A function platform can start many instances at once, each with its own pool.
A pooler changes the arithmetic. Your functions open many cheap client connections to the pooler, and the pooler multiplexes them onto a small number of real Postgres backends. The database sees a steady handful of connections instead of one per invocation. That is the whole idea, and everything else in this post is about the side effects of sharing backends between clients.
Platform behaviour matters as well. Vercel's guide says that with Fluid compute, multiple concurrent invocations share the same instance, so they can share one connection or pool instead of opening a new connection per request. It also warns against a maximum pool size of 1, because that doesn't reduce total connections and limits concurrency; it suggests keeping the minimum pool size at 1 instead. If you are on a different platform, check whether its instances share pools before you copy those settings.
Do suspended function instances leak connections?
They can. In a August 2025 post by Malte Ubl, Vercel explains that the timer a connection pool uses to close idle clients never fires while a function is suspended. The connection stays open until the instance shuts down or the server-side pooler closes it. Vercel's connection pooling guide describes the same behaviour and recommends the attachDatabasePool helper, which closes idle connections before an instance suspends.
This suggests one practical check: if errors cluster right after deploys, old instances may still be holding connections while new ones open theirs. That is my inference from the documented mechanism, not something either post measures.
What does PgBouncer transaction mode break, and what works now?

Transaction mode lends a real Postgres connection to a client for one transaction and then returns it to the pool, which is why it suits serverless. Anything that depends on session state, such as SET, LISTEN or session-level advisory locks, stops working.
PgBouncer's feature map lists SET/RESET, LISTEN, WITH HOLD CURSOR, SQL-level PREPARE/DEALLOCATE, temporary tables with PRESERVE/DELETE ROWS, LOAD and session-level advisory locks as unsupported in transaction pooling. The same page says transaction pooling does support protocol-level prepared plans once max_prepared_statements is set to a non-zero value.
That last point is newer than a lot of advice online. The PgBouncer 1.21.0 release notes, dated 16 October 2023, added protocol-level named prepared statements and report performance gains of 15% to 250% depending on the workload. The configuration reference lists the default of max_prepared_statements as 200 for transaction and statement pooling. SQL-level PREPARE and EXECUTE commands are still forwarded to Postgres without tracking, so they remain unsafe.
Is Supabase's pooler PgBouncer?
The shared pooler on Supabase is Supavisor, and PgBouncer is what the paid dedicated pooler runs. Supabase's connection docs say port 5432 reaches Postgres directly or Supavisor in session mode, while port 6543 reaches PgBouncer for the dedicated pooler and Supavisor for shared transaction mode.

The same page says transaction mode "does not support prepared statements" and tells you to turn them off in your connection library, so on the shared pooler the PgBouncer 1.21 change doesn't help you. Supabase also announced that Supavisor would deprecate session mode on port 6543 on 28 February 2025, leaving 6543 for transaction mode only. Use 6543 from functions, and 5432 or a direct connection for migrations and anything that needs session state.
Two more details from the same page affect planning. Direct connections are on IPv6, or on IPv4 if the project has the IPv4 add-on, while the shared pooler is IPv4-only on every plan. If your runtime can only reach IPv4 hosts, the pooler is how you reach the database at all. And the dedicated pooler is available on paid plans only and runs in transaction mode only, so a feature that needs session state still has to go through port 5432 or a direct connection.
How do you configure Prisma for a pooled connection?
Point Prisma Client at the pooled URL, point migrations at a direct URL, and check your PgBouncer version before adding pgbouncer=true. Prisma's PgBouncer guide recommends not setting that flag on PgBouncer 1.21.0 or later.
Prisma also states why the flag existed: it uses prepared statements, and setting max_prepared_statements above 0 lets PgBouncer handle them. So the decision is about the pooler in front of you. Older PgBouncer versions need the flag to stop Prisma from sending named prepared statements. Versions from 1.21.0 with the setting configured don't.
The same guide explains why migrations need their own connection: the schema engine uses a single connection and does not support pooling through PgBouncer, so the CLI should read a directUrl. A minimal datasource looks like this:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // pooled
directUrl = env("DIRECT_URL") // direct, for migrations
}
The table compares the three poolers you are most likely to meet. Verify each row against the provider's current docs before you rely on it.
| Pooler | What it is | Prepared statements | Migrations go to |
|---|---|---|---|
| Self-hosted PgBouncer 1.21+ | The original pooler | Protocol-level, with max_prepared_statements above 0 | A direct connection |
| Supabase shared pooler | Supavisor, port 6543 for transaction mode | Not supported in transaction mode, per Supabase docs | Session mode on 5432 or a direct connection |
| Neon pooler | Managed PgBouncer, add -pooler to the endpoint ID | Protocol-level supported; SQL-level PREPARE is not | The non-pooled hostname |
Neon's pooling documentation lists pool_mode=transaction, max_client_conn=10000, default_pool_size=0.9 * max_connections and max_prepared_statements=1000. A Neon changelog entry from 27 February 2026 says the PgBouncer version behind its pooling was updated to 1.25.1, so the old habit of adding pgbouncer=true to a Neon string deserves a second look. If you are setting up a Next.js and Prisma stack, the choice between Drizzle and Prisma changes how your driver issues prepared statements, so test that path too.
How do you tell which layer is failing?
Work from the outside in, because three separate layers can produce connection errors and each has a different fix.
- Read the host and port in the connection string your runtime uses. A direct host means no pooler is involved at all.
- If a pooler is involved, find out which mode it runs. On Supabase, port 6543 is transaction mode and port 5432 is session mode.
- If queries fail only on the pooled URL, look for session features from the unsupported list above, such as
SET,LISTENor temporary tables. - If only migrations fail, confirm they read the direct URL and not the pooled one.
This order keeps you from tuning pool sizes when the real cause is a session feature or a wrong URL.
What order should you set pooling up in?
- Find the pooled and the direct connection string for your provider. They are different URLs.
- Put the pooled, transaction-mode URL in the variable your runtime queries read.
- Put the direct URL in the variable your migration and introspection commands read.
- Check the PgBouncer version behind your pooler before adding
pgbouncer=true. - On Vercel, attach your pool with
attachDatabasePoolso idle connections close before the instance suspends. The guide suggests a short idle timeout and gives 5 seconds as an example. - Test a deploy under load, since that is when old and new instances overlap.
What changed recently on the Neon API?
Neon's 1 May 2026 changelog states that pooler_mode and pgbouncer_settings on compute endpoints in the Management API are deprecated, with sunset after 20 June 2026. That date has passed. If your infrastructure code still sets those fields, read the current API reference and remove them.
How much does pooling matter for a small product?
It matters less than the threads suggest. The Pizzeria Bestek case study describes a React and Supabase ordering platform that handles five to ten orders per day. At that volume, I'd expect a function-per-request app to stay far below any connection limit, though the case study doesn't report connection counts, so treat that as my inference.
Pooling is a scale and burst concern, and the SaaS MVP stack I recommend doesn't need tuning on day one. The moment to act is the first too many connections error, or a move to a platform that spins up many instances at once.
Run SHOW max_connections; and then SELECT count(*) FROM pg_stat_activity; against your database right now. If the second number is already more than half of the first while traffic is quiet, your functions are holding connections they should have closed.
