What this usually means
The Prisma query engine's connection pool is exhausted because connections are not returned quickly enough or are held open by uncommitted transactions. This is rarely a simple capacity issue; it's usually caused by micro-bursts (sudden spike in concurrent queries), long-running queries blocking pool slots, or stalled transactions that never commit/rollback. In serverless environments, it can also be caused by connection pooler (e.g., PgBouncer) misconfiguration where `pool_size` in Prisma exceeds the pooler's limit, causing connections to queue up.
The first ten minutes — establish facts before touching code.
- 1Run `prisma --version` to confirm Prisma version; known pool timeout bugs existed pre-5.0.0.
- 2Check `prisma:engine` debug logs: set `DEBUG='prisma:engine'` and look for `waiting for a connection` or `acquire connection timeout`.
- 3Query `pg_stat_activity` on the database: `SELECT state, wait_event, query FROM pg_stat_activity WHERE datname = current_database();` — look for many `idle in transaction` or long-running `active` queries.
- 4Inspect application-level transaction usage: search for `prisma.$transaction` calls that might not have `await` or are missing error handling (try/catch with `rollback`).
- 5Check PgBouncer configuration if used: `SHOW POOLS;` on the PgBouncer admin console to see actual client connections vs pool size.
The specific files, logs, configs, and dashboards that usually own this bug.
- search`/var/log/app/error.log` or `journalctl -u app.service` — Prisma error messages and stack traces.
- searchDatabase logs (PostgreSQL `log_line_prefix` with `%t %u %d`) to capture connection/disconnection events.
- searchPrisma engine logs via `DEBUG='prisma*'` environment variable.
- search`pg_stat_activity` and `pg_stat_statements` for query-level metrics.
- searchApplication metrics dashboard (Datadog, Prometheus) for `prisma_pool_connections_used` and `prisma_pool_connections_idle` (if instrumented).
- searchCloud provider RDS/Cloud SQL metrics for `DatabaseConnections` and `ActiveTransactions`.
- searchPgBouncer `show stats` and `show pools` output.
Practical causes, not theory. These are the things you will actually find.
- warningMicro-bursts: A sudden spike in concurrent requests (e.g., cron job or webhook) exhausts the pool before connections can be recycled.
- warningStalled transactions: A `$transaction` without `await` or missing `catch` that fails to call `rollback`, leaving connections in `idle in transaction` indefinitely.
- warningPgBouncer pool size mismatch: Prisma's `connection_limit` > PgBouncer's `default_pool_size`, causing Prisma to queue connections that PgBouncer cannot serve.
- warningLong-running queries (e.g., missing index, full table scans) hold connections for seconds, reducing effective pool capacity.
- warningConnection leak from incorrect `$disconnect` usage: calling `$disconnect` manually in middleware without `finally` block can close the pool while connections are in use.
- warningPrisma engine process restart: the query engine can crash silently due to OOM, causing all connections to drop and be re-acquired, leading to temporary exhaustion.
Concrete fix directions. Pick the one that matches your root cause.
- buildSet `connection_limit` to a value that matches the database's max_connections minus reserved slots. Use `connection_limit = max_connections - 10` as a starting point.
- buildImplement connection pool timeout: `connection_timeout = 5000` (ms) to fail fast instead of hanging 10s.
- buildAdd retry logic with exponential backoff for transient `P2024` errors using a library like `p-retry`.
- buildWrap all `$transaction` calls in try/catch with `rollback` in `finally` block. Use `$transaction` with the `timeout` option.
- buildFor serverless, use `PrismaClient` with `connection_limit = 1` and ensure `$disconnect` is called after each request (use `prisma.$on('beforeExit', ...)`).
- buildAdd connection pooling middleware: use `pg-pool` or `pgBouncer` to pool database connections at the transport layer, then set Prisma's `connection_limit` to the pooler's limit.
- buildMonitor and alert on `prisma_pool_connections_used` vs `prisma_pool_connections_limit` to catch exhaustion early.
A fix you cannot prove is a guess. Close the loop.
- verifiedDeploy fix and run load test (e.g., `artillery` or `k6`) with concurrency equal to 2x `connection_limit`; confirm no `P2024` errors.
- verifiedMonitor `pg_stat_activity` during peak load: verify idle connection count stays near zero but active connections remain below limit.
- verifiedCheck Prisma client metrics (if using OpenTelemetry) for `prisma_pool_connections_used` staying below `connection_limit`.
- verifiedUse `prisma.$queryRaw` to run `SELECT count(*) FROM pg_stat_activity WHERE state = 'idle in transaction'` before and after fix; should drop to zero.
- verifiedSimulate a stalled transaction: remove a rollback intentionally in a test environment and verify that the pool now recovers within `connection_timeout`.
- verifiedFor PgBouncer: run `SHOW POOLS;` and confirm that `cl_active` never reaches `pool_size` for more than a few seconds.
Things that make this bug worse or harder to find.
- warningCranking up `connection_limit` blindly — this can exhaust database `max_connections` and cause `FATAL: sorry, too many clients already`.
- warningNot setting `connection_timeout` — the default 10s can cause cascading timeouts across services.
- warningAssuming the issue is capacity when it's a transaction leak: adding more connections just masks the problem.
- warningUsing `$disconnect` in middleware without a `finally` block — can close the pool prematurely.
- warningIgnoring `idle_in_transaction_session_timeout` on the database — set it to 5s to auto-kill stalled transactions.
- warningSetting `connection_limit` higher than PgBouncer's pool size when using connection pooling — leads to queueing.
The 10-Second Hang: A Prisma Pool Timeout in Production
Timeline
- 10:00Deploy new GraphQL endpoint that runs 3 parallel Prisma queries inside a $transaction.
- 10:15Alert: p99 latency spikes from 200ms to 10s.
- 10:17First look: CPU and memory normal, but RDS connections maxed at 100 (max_connections=200).
- 10:20Check `pg_stat_activity`: 50 connections in 'idle in transaction' from our app.
- 10:22Enable DEBUG='prisma*' and see 'waiting for a connection from the pool' on every request.
- 10:25Identify the culprit: GraphQL resolver uses $transaction but missing rollback on error.
- 10:30Hotfix: wrap transaction in try/catch with rollback in finally. Deploy.
- 10:35Latency drops to 250ms, connections normalize.
We had just deployed a new GraphQL mutation that fetched data from three related tables inside a Prisma `$transaction`. Within minutes, our p99 latency shot up to 10 seconds—exactly the default Prisma pool timeout. My first instinct was to check CPU and memory, but both looked fine. I then checked RDS connections and saw they were maxed at 100, though our limit was 200. Something was holding connections open.
I queried `pg_stat_activity` and found 50 connections from our app in 'idle in transaction' state. That's a classic sign of a transaction that started but never committed or rolled back. I enabled `DEBUG='prisma*'` and saw every new request logging 'waiting for a connection from the pool' because all 50 connections were stuck. The pool was exhausted.
I traced the code to our GraphQL resolver: it used `prisma.$transaction` but the error handler was missing a `catch` block—when an exception occurred, the transaction was never rolled back, so the connection remained open. I added a try/catch with `rollback` in a `finally` block and deployed a hotfix. Latency dropped immediately. Later, I also set `idle_in_transaction_session_timeout` to 10s on the database and added monitoring for transaction states.
Root cause
A Prisma $transaction without proper error handling left connections in 'idle in transaction' state, exhausting the pool.
The fix
Wrapped all $transaction calls in try/catch/finally with `prisma.$disconnect()` in finally (or rollback on error). Also set `idle_in_transaction_session_timeout = 10000` on PostgreSQL.
The lesson
Always ensure transactions are properly finalized. Monitor `pg_stat_activity` for idle-in-transaction as a key health metric. Set timeouts at both application and database levels.
Prisma uses a dedicated query engine binary that maintains its own connection pool. The pool size is set via `connection_limit` in the PrismaClient constructor. By default, it's `num_physical_cpus * 2 + 1`. Each connection is a TCP socket to the database. When a request comes in, the engine checks if an idle connection is available; if not, it waits up to `connection_timeout` (default 10s) for a connection to become free.
The pool is per PrismaClient instance. In serverless environments, each Lambda cold start creates a new pool, so you must limit connections to 1 and cache the client. The query engine can be observed via `DEBUG='prisma:engine'`, which logs pool states like `acquiring connection` and `releasing connection`.
A micro-burst is a sudden spike in concurrency that exceeds the pool for a few seconds, then subsides. For example, a webhook that triggers 50 parallel requests. The pool may have 20 connections; the first 20 go through, the remaining 30 queue. If the burst lasts longer than `connection_timeout`, those 30 requests fail. The fix is to either increase pool size (if database can handle it) or implement request queuing with backpressure at the application level.
Sustained load means the pool is consistently saturated. In that case, you need to increase pool size or optimize queries. Use `pg_stat_statements` to find slow queries and add indexes. Also consider read replicas for read-heavy workloads.
Many teams use PgBouncer in transaction mode to multiplex client connections. Prisma's query engine talks to PgBouncer via a single TCP connection per pool slot. If Prisma's `connection_limit` is larger than PgBouncer's `default_pool_size`, Prisma will open more connections than PgBouncer can handle, causing queuing at the PgBouncer level. The symptom is Prisma timeout even though PgBouncer shows all connections in use.
Solution: set Prisma's `connection_limit` equal to PgBouncer's `default_pool_size`. Also ensure PgBouncer's `max_client_conn` is high enough to handle the total number of Prisma pool slots across all application instances.
Prisma exposes metrics via OpenTelemetry if you enable `tracing`. Key metrics: `prisma_pool_connections_used`, `prisma_pool_connections_idle`, and `prisma_pool_connections_limit`. Set an alert when `used / limit > 0.8` for more than 1 minute.
Database-side: monitor `pg_stat_activity` for state counts. Alert on `idle in transaction` > 5 for more than 30 seconds. Use `pg_stat_statements` to identify queries that take > 1s. Combine with application-level tracing to correlate slow queries with pool pressure.
Always use the `$transaction` API with a callback that returns a promise. The callback should be async and handle errors internally. If any operation fails, the transaction is automatically rolled back. Never manually call `$transaction` without a callback; it returns a promise that you must await and handle.
For long-running transactions, set a timeout using the `timeout` option (in milliseconds) to automatically cancel if it takes too long. Example: `prisma.$transaction([...], { timeout: 5000 })`. This prevents a transaction from holding a connection indefinitely.
Frequently asked questions
How do I check the current Prisma connection pool size at runtime?
Prisma doesn't expose a direct API to read the pool size at runtime, but you can infer it from the `connection_limit` you set in the PrismaClient constructor. Enable debug logs with `DEBUG='prisma:engine'` to see pool state changes. Alternatively, use OpenTelemetry metrics if you have tracing enabled.
What is the difference between `connection_limit` and `connection_timeout`?
`connection_limit` sets the maximum number of concurrent connections in the pool. `connection_timeout` sets how long (in ms) a request will wait for a connection to become available before throwing a timeout error. Default `connection_timeout` is 10000 (10s). You should set it lower (e.g., 3000) to fail fast and avoid cascading timeouts.
Should I use PgBouncer with Prisma?
It depends. If you have many short-lived connections (e.g., serverless), PgBouncer in transaction mode reduces connection overhead. However, Prisma's query engine already pools connections, so adding PgBouncer can add complexity. If you use it, ensure Prisma's `connection_limit` matches PgBouncer's pool size to avoid queueing. Also, test with your workload because Prisma uses prepared statements and PgBouncer's transaction mode may not be fully compatible with all Prisma features.
What does `P2024` error mean?
`P2024` is a Prisma-specific error code for 'connection pool timeout'. It indicates that the query engine could not acquire a connection from the pool within the `connection_timeout` period. The error message includes the timeout duration. This is different from a database connection refused error (`ECONNREFUSED`), which means the database server is not reachable.
How do I prevent connection leaks in serverless environments?
In serverless (e.g., AWS Lambda), create a single PrismaClient instance outside the handler and reuse it across invocations. Call `prisma.$disconnect()` at the end of each invocation (in a `finally` block) to return the connection to the pool. Set `connection_limit = 1` to avoid connection overuse. Also, consider using a connection pooler like PgBouncer to handle many concurrent invocations.