Prisma Connection Pool Timeouts in Production: What Actually Fixes Them
Traffic picks up, and about a third of the requests start returning 500. The application logs are a wall of the same thing over and over. Somebody finds the connection limit setting, raises it from 9 to 50, redeploys, and the errors stop. Everyone goes back to work.
Ninety minutes later Postgres starts refusing connections outright and the whole service is down instead of degraded.
This is the single most common way I see Prisma pool timeouts handled, and it is worse than doing nothing, because it converts a slow service into an outage. The pool timeout is a queueing symptom. Queues form for exactly two reasons: too many clients arriving at once, or each client holding a connection for too long. Raising connection_limit addresses only the first, and only until you hit the Postgres max_connections wall, at which point you have moved the error rather than removed it.
- Work out your real ceiling first:
app instances × connection_limitmust stay comfortably below Postgresmax_connections. On serverless, "app instances" is the number of concurrent function instances, which you do not control. - If you are on serverless or have more than a handful of instances, put PgBouncer in transaction mode (or Prisma Accelerate) between Prisma and Postgres and set
connection_limit=1per instance. That is the fix. A bigger pool is not. - Set
connection_limitandpool_timeoutexplicitly inDATABASE_URLrather than relying on the default ofnum_physical_cpus * 2 + 1. - Audit interactive
$transactioncallbacks for network calls. A transaction that awaits an HTTP request holds a pooled connection for the entire round trip. - Only then consider raising the limit, and raise
max_connectionsto match if you do.
The exact error
PrismaClientKnownRequestError:
Invalid `prisma.user.findMany()` invocation:
Timed out fetching a new connection from the connection pool.
More info: http://pris.ly/d/connection-pool
(Current connection pool timeout: 10, connection limit: 10)
The error code is P2024. Older Prisma versions phrase it as "Timed out fetching a new connection from the pool", so search for both. The two numbers in the parentheses are the most useful part of the message and almost everyone skips them: they are your current pool_timeout in seconds and your current connection_limit. If those are not the values you thought you configured, you have already found your bug.
Why this happens
The arithmetic nobody does
Prisma keeps its own connection pool inside the query engine. By default the size is num_physical_cpus * 2 + 1, so a four-core machine gets nine connections. The default acquire timeout is 10 seconds. A query that cannot get a connection within that window throws P2024.
Now the part that decides whether you are fine or on fire. The number of connections you need is roughly the number of queries in flight at any moment, which is your request rate multiplied by how long each query holds its connection. Ten requests per second where each request holds a connection for 100ms needs about one connection. Ten requests per second where a query takes 2 seconds needs about twenty. Query duration is a multiplier on pool pressure. That is why "add more connections" so often fails: you have a slow query, and you are buying more slots for it to be slow in.
So before touching any config, find out whether your queries got slower. If your database is MySQL or MariaDB, the slow query log is the fastest way to find the query doing the damage; on Postgres, pg_stat_statements ordered by total_exec_time does the same job.
Serverless multiplies your pool by a number you do not control
This is the one that catches teams moving from a VPS to Vercel or Lambda. On a long-running server there is one process and one pool. In serverless, every concurrent function instance is its own process with its own complete Prisma pool. Fifty concurrent instances at the default nine connections each is 450 connections arriving at a Postgres that, on most managed plans, is configured for a fraction of that.
Long transactions hold connections through things that are not database work
An interactive transaction takes a connection out of the pool and does not give it back until the callback resolves. So this innocent-looking code removes one connection from a pool of nine for as long as a third-party API takes to respond:
// Do not do this.
await prisma.$transaction(async (tx) => {
const order = await tx.order.create({ data })
const receipt = await stripe.charges.create({ ... }) // network call
await tx.order.update({ where: { id: order.id }, data: { receipt: receipt.id } })
})
Under load, five concurrent orders against a slow payment provider is five connections gone. The Prisma docs say this directly: "Try to avoid performing network requests and executing slow queries inside your transaction functions." The defaults reinforce it, with maxWait at 2000ms and timeout at 5000ms. If you have raised those because transactions were timing out, you have very likely made your pool problem worse in order to fix a symptom of the same underlying design.
The fix, step by step
Step 1: Find out what your limits actually are
SHOW max_connections;
SELECT count(*), state
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state;
Expect max_connections to be 100 on a default install and often lower on small managed plans. If idle in transaction is a large share of the second query's output, you have found long transactions and you can skip straight to step 4.
Step 2: Set the pool explicitly instead of inheriting a default
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20"
Both are query parameters on the connection string. Setting them explicitly means the numbers in your P2024 message match something you wrote down, which makes every future debugging session shorter. Note that Prisma ORM v7 driver adapters use a different default pool size from the v6 query engine, so if you migrated and the behaviour changed, check this first.
A longer pool_timeout is not a fix on its own. It converts a fast error into a slow request, which is sometimes what you want during an incident and never what you want as a permanent setting.
Step 3: Put a pooler in front, especially on serverless
PgBouncer in transaction mode is the standard answer. It accepts many client connections and multiplexes them onto a small pool of real Postgres connections, released at the end of each transaction rather than the end of each client session. Prisma needs to be told it is talking to a transaction-mode pooler, because prepared statements do not survive that:
DATABASE_URL="postgresql://user:pass@pgbouncer-host:6432/db?pgbouncer=true&connection_limit=1"
DIRECT_URL="postgresql://user:pass@postgres-host:5432/db"
connection_limit=1 looks alarming and is correct here: each short-lived instance needs one connection, and the pooler does the sharing. DIRECT_URL is used for migrations, which need a real session connection and must bypass the pooler. Wire that into your Prisma schema's datasource block alongside url.
If you would rather not run PgBouncer yourself, Prisma Accelerate provides the same external pooling as a managed service, and most managed Postgres providers now ship a pooled endpoint on a separate port. Any of the three solves the same problem. Running none of them on serverless does not.
Step 4: Get network calls out of transactions
Rewrite the pattern above so the database work happens in two short transactions with the slow call in between, and make the second write idempotent so a crash in the middle is recoverable. If you genuinely need atomicity across a remote call, you want an outbox table and a background worker, not a longer transaction. This is the same discipline as handling payment callbacks that fire more than once: assume the step can be repeated, and make repetition harmless.
Step 5: Make sure you have one PrismaClient, not many
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Every new PrismaClient() creates its own pool. In development with hot reloading, a module that instantiates one at import time will create a fresh pool on every reload until the database refuses connections. This is a real and extremely common cause of P2024 on a laptop with no traffic at all.
Verification
-- Total connections should now sit well under max_connections under load
SELECT count(*) FROM pg_stat_activity WHERE datname = current_database();
-- And almost nothing should be parked in a transaction
SELECT count(*) FROM pg_stat_activity
WHERE datname = current_database() AND state = 'idle in transaction';
Then run your load test again and watch three things together: the count above, the P2024 rate, and p95 latency. The outcome you want is connection count flat, P2024 at zero, and latency unchanged. If P2024 goes to zero but latency climbs, you did not fix the queue, you moved it into pool_timeout.
What people get wrong
"Raise connection_limit until the errors stop." This works right up to max_connections, and then Postgres starts rejecting connections and you are fully down instead of partly slow. Every Postgres connection is also a backend process with its own memory, so a large pool costs real RAM on the database server, which on a modest VPS is exactly the resource you have least of. If you raise the pool, raise max_connections deliberately and size the machine for it.
"Raise pool_timeout to 60 seconds." Now requests queue for a minute before failing, your web server's worker pool fills with waiting requests, and the failure spreads outward from the database into the application tier. Long queues are how a database problem becomes a site-wide outage.
"Use PgBouncer in session mode, it is safer." Session mode assigns a server connection for the whole client session, which means it does almost nothing for the serverless fan-out you are trying to fix. Transaction mode is the point. Yes, it disables prepared statements, which is why pgbouncer=true exists.
"Add a retry around the query." Retrying into a saturated pool adds load to the thing that is already saturated. If you retry at all, do it with a cap and a backoff, and treat it as a way to survive a thirty-second blip rather than as the fix.
When it is still broken
- Check whether the pool is even the bottleneck. If
pg_stat_activityshows plenty of idle connections while Prisma reports P2024, you have multiple pools that cannot see each other, which usually means multiple PrismaClient instances or multiple processes. - Look for a missing index. A sequential scan that used to take 40ms takes 4 seconds once the table grows, and pool exhaustion is the first place you notice it. Fix the query and the pool problem disappears on its own.
- Check for lock contention. Query
pg_locksjoined topg_stat_activityfor blocked queries. Connections waiting on a row lock are held just as long as connections doing work. - Check your provider's own connection cap. Managed Postgres plans frequently enforce a limit lower than the
max_connectionsthe server reports, and the pooled endpoint they give you exists precisely for this reason.
The framing I would leave you with: connections are not a resource you scale, they are a resource you conserve. Every improvement that matters here, faster queries, shorter transactions, an external pooler, works by holding connections for less time. The ones that do not work all involve typing a bigger number.
Frequently asked questions
- What does the Prisma error P2024 Timed out fetching a new connection from the connection pool mean?
- It means a query waited longer than pool_timeout, 10 seconds by default, without a free connection becoming available in Prisma's internal pool. The parentheses at the end of the message print your current pool_timeout and connection_limit, which is the fastest way to confirm whether your configuration is being applied. It is a queueing symptom, caused either by too many concurrent queries or by connections being held too long.
- What is the default Prisma connection pool size?
- For the Prisma ORM v6 query engine the default pool size is num_physical_cpus * 2 + 1, so a four-core machine gets nine connections, and the default acquire timeout is 10 seconds. Prisma ORM v7 driver adapters use a different default, so behaviour can change on upgrade. Set connection_limit and pool_timeout explicitly as query parameters on DATABASE_URL rather than relying on either default.
- Why do I get connection pool errors on Vercel or Lambda but not on a normal server?
- Because every concurrent function instance is a separate process with its own complete Prisma pool. Fifty concurrent instances at nine connections each attempt 450 connections against a database often configured for 100. The fix is an external pooler such as PgBouncer in transaction mode or Prisma Accelerate, with connection_limit=1 per instance, not a larger pool.
- Should I just increase connection_limit to fix Prisma pool timeouts?
- Only after you have ruled out slow queries and long transactions, and only if you also raise Postgres max_connections and size the database server for it. Each Postgres connection is a backend process with its own memory, so a large pool has a real cost, and once you exceed max_connections the database refuses connections outright and a degraded service becomes a full outage.