fix(prisma): cache de client in álle omgevingen + honoreer connection_limit #171

Merged
janpeter merged 1 commit from fix/prisma-singleton-pool into main 2026-08-16 15:43:53 +02:00
Owner

Waarom

scrum4me-postgres liep op 2026-08-16 vol op 100/100 client backends — inclusief de 3 superuser_reserved_connections, waardoor zelfs psql er niet meer in kon en pg_terminate_backend() onbereikbaar was (dat vereist zelf een connectie). Eerste FATAL: sorry, too many clients already op 2026-08-15 06:28 UTC, daarna 4.526× in 96 uur.

Eén next-server-proces hield 43 van de 100 connecties vast. Twee defecten in lib/prisma.ts samen:

1. De singleton-guard stond omgekeerd voor productie

export const prisma = globalForPrisma.prisma ?? createPrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma   // ← nooit in prod

In productie werd globalForPrisma.prisma dus nooit gezet. Elke module-scope die lib/prisma importeert draaide createPrismaClient() opnieuw → eigen PrismaClient + eigen pg.Pool. Het standaard-Next.js-patroon beschermt tegen HMR in dev; tegen Next's per-route bundling in prod beschermt het niets. Daarom liep het langzaam vol in plaats van in één klap: elke nieuw geraakte route-bundle kan er tot 10 connecties bij pakken.

2. connection_limit in de URL is dode config bij de driver-adapter

.env heeft ?connection_limit=10&pool_timeout=20, maar new Pool({ connectionString: url }) kreeg geen max. Met de PrismaPg-adapter pooled node-postgres, niet Prisma's Rust-pool — en pg kent die parameter niet. Geverifieerd op de geïnstalleerde pg 8.20.0:

options.max = 10   |   options.connection_limit = undefined

psql weigert dezelfde URL zelfs met invalid URI query parameter: "connection_limit". Wie die waarde bijstelde om het probleem te verhelpen, veranderde dus niets.

Wat deze PR doet

  • Cachet de client op globalThis in álle omgevingen.
  • Nieuwe side-effect-vrije module lib/prisma-pool.ts met poolMaxFromUrl(), die connection_limit uit DATABASE_URL leest en als max aan de pool geeft. Valt terug op 10 bij een ontbrekende, niet-positieve, niet-hele of niet-URL-vormige waarde (pg accepteert ook host=... dbname=...). Eigen module omdat lib/prisma.ts bij import een client aanmaakt en daar dus niets uit te testen valt.
  • Unit-test voor de parsing (5 cases).

Bewust niet in deze PR: de connection string wordt ongewijzigd doorgegeven aan pg. Herschrijven via new URL().toString() zou het wachtwoord kunnen her-encoderen — dat risico is die cosmetiek niet waard.

Verificatie

  • npx vitest run __tests__/lib/prisma-pool.test.ts5/5 pass
  • npx eslint op de drie bestanden → exit 0
  • npm run typecheck296 fouten mét én zónder deze wijziging; diff van de foutregels is leeg. Geen enkele fout verwijst naar lib/prisma*. (De 296 zijn pre-existing in de dev-clone — stale generated client / dirty vendor/scrum4me-shared.)
  • npm test17 falende regels mét én zónder deze wijziging; niets nieuw en niets stilletjes opgelost.

Nog open (buiten deze PR)

Postgres heeft géén idle_session_timeout en géén idle_in_transaction_session_timeout (beide uit), dus er is geen vangnet als een client alsnog connecties laat staan. Dat stond bij het vorige pool-incident (juli, MCP-wezen) al genoteerd als openstaand punt en raakt de gedeelde prod-DB — apart te besluiten.

## Waarom `scrum4me-postgres` liep op 2026-08-16 vol op **100/100** client backends — inclusief de 3 `superuser_reserved_connections`, waardoor zelfs `psql` er niet meer in kon en `pg_terminate_backend()` onbereikbaar was (dat vereist zelf een connectie). Eerste `FATAL: sorry, too many clients already` op 2026-08-15 06:28 UTC, daarna 4.526× in 96 uur. Eén `next-server`-proces hield **43 van de 100** connecties vast. Twee defecten in `lib/prisma.ts` samen: ### 1. De singleton-guard stond omgekeerd voor productie ```ts export const prisma = globalForPrisma.prisma ?? createPrismaClient() if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma // ← nooit in prod ``` In productie werd `globalForPrisma.prisma` dus **nooit gezet**. Elke module-scope die `lib/prisma` importeert draaide `createPrismaClient()` opnieuw → eigen `PrismaClient` + eigen `pg.Pool`. Het standaard-Next.js-patroon beschermt tegen HMR in dev; tegen Next's per-route bundling in prod beschermt het niets. Daarom liep het langzaam vol in plaats van in één klap: elke nieuw geraakte route-bundle kan er tot 10 connecties bij pakken. ### 2. `connection_limit` in de URL is dode config bij de driver-adapter `.env` heeft `?connection_limit=10&pool_timeout=20`, maar `new Pool({ connectionString: url })` kreeg geen `max`. Met de `PrismaPg`-adapter pooled **node-postgres**, niet Prisma's Rust-pool — en `pg` kent die parameter niet. Geverifieerd op de geïnstalleerde `pg` 8.20.0: ``` options.max = 10 | options.connection_limit = undefined ``` `psql` weigert dezelfde URL zelfs met `invalid URI query parameter: "connection_limit"`. Wie die waarde bijstelde om het probleem te verhelpen, veranderde dus niets. ## Wat deze PR doet - Cachet de client op `globalThis` in **álle** omgevingen. - Nieuwe side-effect-vrije module `lib/prisma-pool.ts` met `poolMaxFromUrl()`, die `connection_limit` uit `DATABASE_URL` leest en als `max` aan de pool geeft. Valt terug op 10 bij een ontbrekende, niet-positieve, niet-hele of niet-URL-vormige waarde (`pg` accepteert ook `host=... dbname=...`). Eigen module omdat `lib/prisma.ts` bij import een client aanmaakt en daar dus niets uit te testen valt. - Unit-test voor de parsing (5 cases). Bewust **niet** in deze PR: de connection string wordt ongewijzigd doorgegeven aan `pg`. Herschrijven via `new URL().toString()` zou het wachtwoord kunnen her-encoderen — dat risico is die cosmetiek niet waard. ## Verificatie - `npx vitest run __tests__/lib/prisma-pool.test.ts` → **5/5 pass** - `npx eslint` op de drie bestanden → **exit 0** - `npm run typecheck` → **296 fouten mét én zónder** deze wijziging; diff van de foutregels is **leeg**. Geen enkele fout verwijst naar `lib/prisma*`. (De 296 zijn pre-existing in de dev-clone — stale generated client / dirty `vendor/scrum4me-shared`.) - `npm test` → **17 falende regels mét én zónder** deze wijziging; niets nieuw en niets stilletjes opgelost. ## Nog open (buiten deze PR) Postgres heeft géén `idle_session_timeout` en géén `idle_in_transaction_session_timeout` (beide uit), dus er is geen vangnet als een client alsnog connecties laat staan. Dat stond bij het vorige pool-incident (juli, MCP-wezen) al genoteerd als openstaand punt en raakt de gedeelde prod-DB — apart te besluiten.
fix(prisma): cache de client in álle omgevingen + honoreer connection_limit
All checks were successful
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m39s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
f3ac40fcf3
De singleton-guard zette globalForPrisma.prisma alleen wanneer
NODE_ENV !== 'production' — precies omgekeerd voor de vloot. In productie was er
dus géén cache: elke module-scope die lib/prisma importeert maakte een eigen
PrismaClient met een eigen pg.Pool. Next bundelt lib/prisma per route-chunk, dus
dat schaalde mee met het aantal geraakte routes.

Daarnaast negeert node-postgres de Prisma-eigen URL-parameter connection_limit:
met de PrismaPg-adapter pooled pg, niet Prisma. Elke pool viel stil terug op de
pg-default van 10 en `?connection_limit=10&pool_timeout=20` in DATABASE_URL deed
niets. poolMaxFromUrl leest hem nu expliciet uit, met de default als vangnet.

Gemeten op prod 2026-08-16: één next-server-proces hield 43 van de 100 connecties
vast (~4-5 pools x 10). scrum4me-postgres zat op 100/100 inclusief de 3
superuser_reserved slots, waardoor zelfs pg_terminate_backend onbereikbaar was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/Scrum4Me!171
No description provided.