#30275·Prisma

Prisma Postgres pooler stalls a new client’s first query after ~15 minutes idle

Author: davidtheladderCreated Sep 13, 2026Updated Sep 13, 2026

Package and version

[email protected] @prisma/[email protected]

What happened?

Prisma Postgres pooler (pooled.db.prisma.io, Vercel Marketplace) stalls a new client's first statement when it hands out a server connection that sat idle ~15 minutes. The query never returns. Prisma has no bound on that wait (socket_timeout does not cover prepare_typed), so the request hangs until the host kills it.

We hit this on a Vercel cron (GET /api/cron, every 15 minutes, maxDuration = 300). After switching production DB_URL from the direct host (db.prisma.io) to the pooled host (2026-09-08), every other cron tick timed out:

  • success ~1s → leave one server connection idle in the pooler
  • ~899s later the next client gets that connection
  • first statement (SELECT 1 / $executeRaw) never gets a reply
  • Vercel 504: "Task timed out after 300 seconds" (~40 × 504/day)
  • pooler then drops that server connection; the next tick succeeds; cycle repeats

During a hang, pg_stat_activity (via the direct host) showed our backend idle / ClientRead, pg_locks empty, no running query. The hang also happens on a brand-new serverless process with no prior Prisma connection, so it is not a stale client socket. connect_timeout never fires (TCP+TLS+auth already succeeded). Setting socket_timeout=30 did not help.

A second SELECT 1 issued while the first is still pending returns immediately on a new pooled connection.

What did you expect to happen?

The first query on a pooled connection should return or fail within connect_timeout / socket_timeout. The pooler should reset or discard a server connection that has been idle ~15 minutes instead of handing it to a new client in a half-dead state. socket_timeout should also cover the prepare path (prepare_typed), not only execute.

Minimal reproduction

Cannot share the production DB. Repro is timing + the Prisma Postgres pooler, not schema:

  1. Prisma Postgres via Vercel Marketplace. App uses the pooled URL (pooled.db.prisma.io) with connection_limit=5&pool_timeout=10. Do not add pgbouncer=true (Prisma Postgres already tracks prepared statements).

  2. Prisma ORM 6.19, Next.js on Vercel, a */15 * * * * cron whose first statement is:

    await prisma.$executeRawSELECT 1; // then any cheap reads

  3. Let a run succeed (opens a server connection, then the instance goes idle).

  4. Wait ~15 minutes with no other traffic on that pooled connection.

  5. Hit the cron again.

Expected: SELECT 1 returns in <1s. Actual: it never returns; function 504s at 300s. Next tick after the kill succeeds.

Workaround that recovered every stalled tick: race the operation against 8s and re-issue the read on a new pooled connection. Writes are not retried.

Environment

  • Node: 24.x on Vercel (local 24.13.1)
  • OS: Vercel Fluid / iad1 (local macOS 15)
  • Package manager: npm
  • Database: Prisma Postgres (Vercel Marketplace), pooled host pooled.db.prisma.io, direct host db.prisma.io
  • Prisma ORM: 6.19.2 (library engine)
  • Framework: Next.js 16.1 on Vercel
  • Cron: GET /api/cron every 15 minutes

Additional context

Example recovered tick after our workaround (same stall, then re-issue):

  • 2026-09-13 12:00:33 UTC
  • deployment dpl_AMCMLbxjapx4phrsxBPLWv4tyEtj
  • log: {"evt":"prisma.stall","model":null,"operation":"$queryRaw","attempt":1,"reissue":true,"stallMs":8000}
  • then HTTP 200 in 8.6s (was 504 / 300s before the workaround)

Questions:

  1. What is the Prisma Postgres pooler’s idle / reset behaviour around ~900s?
  2. Can socket_timeout cover prepare_typed?
  3. Can the pooler reset a ~15-min-idle server connection instead of handing it out half-dead?

Happy to share more Vercel request IDs. Please do not ask for the connection string.