// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // URL-ul prin PgBouncer (port 6432, de obicei)
directUrl = env("DIRECT_URL") // Conexiunea directă la DB pentru migrări (port 5432)
}
// .env
// Observă pgbouncer=true la finalul conexiunii prin bouncer
DATABASE_URL="postgresql://user:pass@db-bouncer-host:6432/mydb?pgbouncer=true&connection_limit=5"
DIRECT_URL="postgresql://user:pass@db-direct-host:5432/mydb"Să fim serioși, Prisma este un ORM genial pentru productivitate, dar are o mare problemă: mănâncă conexiuni la baza de date pe pâine. Fiecare instanță de microserviciu sau funcție serverless vrea propriul pool de conexiuni. Am pățit asta acum un an la un proiect cu peste 12.000 de utilizatori activi simultan, unde Postgres-ul pur și simplu a îngenuncheat din cauza overhead-ului de context switching.
Soluția clasică este să pui PgBouncer în față. Totuși, dacă îl configurezi „la ochi”, riști să strici totul, de la tranzacții eșuate la erori ciudate de prepared statements.
Session vs Transaction: Alegerea care te salvează sau te îngroapă
PgBouncer poate rula în mai multe moduri, dar în producție te lovești de două: session și transaction.
În Session mode, când aplicația ta cere o conexiune, PgBouncer îi alocă una din pool și o lasă blocată acolo până când aplicația se închide complet. Pentru serverless sau containere care se scalează agresiv, modul ăsta e aproape inutil. Nu rezolvă problema limitării fizice a conexiunilor din Postgres.
În Transaction mode, conexiunea fizică la Postgres este eliberată și returnată în pool imediat ce o tranzacție SQL s-a terminat. Aplicația ta crede că are încă o conexiune deschisă, dar în spate, PgBouncer jonglează cu ele.
Trade-off-ul major: În transaction mode pierzi suportul nativ pentru prepared statements (interogări pre-compilate). Dacă aplicația încearcă să ruleze un prepared statement pe o conexiune fizică diferită de cea pe care a fost creat, Postgres va arunca o eroare masivă de tipul prepared statement already exists sau does not exist. Prisma folosește prepared statements implicit, deci aici apare conflictul.
Cum configurezi Prisma să nu spargă PgBouncer
Ca să folosești modul transaction cu Prisma, trebuie să-i spui explicit ORM-ului că folosește un bouncer. Altfel, va încerca să optimizeze interogările prin metode care vor corupe sesiunile din bouncer.
Trucul este să folosești două URL-uri de conexiune diferite în schema ta. Ai nevoie de un URL direct pentru migrări (unde ai nevoie de drepturi de session pentru a bloca tabelele în siguranță) și un URL prin PgBouncer pentru traficul curent din aplicație, adăugând parametrul pgbouncer=true.
Acest parametru forțează Prisma să folosească protocolul simplu de interogare (fără prepared statements persistente la nivel de sesiune), prevenind erorile menționate.
Cât de mare facem pool-ul? (Sizing real, nu din tutoriale)
Am văzut mulți developeri care pun max_client_conn = 10000 și default_pool_size = 100 doar pentru că au RAM pe server. Este o greșeală tactică. Fiecare conexiune activă în Postgres consumă memorie (cam 10MB per proces) și pune presiune pe CPU.
Regula mea empirică, testată în producție, este următoarea:
default_pool_size = (număr_procesoare_fizice * 2) + număr_discuri
Dacă ai un server de DB cu 4 vCPUs și SSD-uri rapide, un pool de 15-20 de conexiuni către Postgres este mai mult decât suficient pentru a deservi 500 sau chiar 1000 de conexiuni client dinspre aplicațiile tale prin PgBouncer. Am economisit cam 35% din resursele CPU ale bazei de date doar scăzând pool-ul de la 80 la 20. Postgres lucrează mult mai rapid când nu trebuie să facă juggling între zeci de thread-uri concurente.
Tu cum gestionezi conexiunile în producție? Ai trecut deja pe Supabase Supavisor sau ai rămas fidel bătrânului PgBouncer configurat manual?