datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // Port 6543 (PgBouncer) + ?pgbouncer=true
directUrl = env("DIRECT_URL") // Port 5432 (Postgres direct)
}Dacă folosești Prisma în producție și ai trecut de faza de MVP, sigur te-ai lovit de faimoasa eroare cu max connections reached. Prisma are un obicei prost: își face propriul pool de conexiuni pe fiecare instanță de server. Când ai 3 instanțe în autoscaling, plus niște serverless functions, baza de date crapă rapid.
Am pățit asta acum un an la un proiect cu peste 12k utilizatori activi concurenți. Soluția clasică e PgBouncer, dar dacă îl configurezi greșit, mai rău strici. Hai să vedem cum se face corect, fără bullshit teoretic.
Session vs Transaction Mode: Unde e capcana?
PgBouncer are trei moduri, dar în producție discutăm doar de două: session și transaction.
În Session Mode, PgBouncer se comportă ca un proxy chior. Când o instanță se conectează, primește o conexiune la Postgres și o ține ocupată până când se închide clientul. Pentru Prisma, modul ăsta e aproape inutil. Dacă ai 50 de instanțe de Node.js, tot ai nevoie de 50 de conexiuni deschise în Postgres. Am economisit exact 0% resurse așa.
În Transaction Mode, conexiunea la Postgres e eliberată imediat ce s-a terminat tranzacția curentă. Următorul query din alt client poate folosi aceeași conexiune fizică. Aici e salvarea. Am reușit să deservim aceiași 12k useri cu doar 15 conexiuni reale în Postgres, în loc de 150.
Dar există un mare trade-off. În modul transaction, nu poți folosi Prepared Statements (decât cu niște hack-uri din Postgres 12+) și pierzi funcționalități ca LISTEN/NOTIFY sau tabele temporare. Pentru noi, a meritat sacrificiul.
Cum legi Prisma la PgBouncer (Fără să strici migrările)
Prisma are nevoie de două conexiuni diferite în schema.prisma. De ce? Pentru că migrările (prisma migrate deploy) au nevoie de tranzacții lungi și folosesc comenzi de sistem care crapă în modul transaction al PgBouncer.
Configurația corectă folosește url și directUrl în schema ta.
url trimite query-urile normale prin PgBouncer (portul 6543, de obicei) și îi punem parametrul ?pgbouncer=true. Acest parametru îi spune motorului Prisma să nu folosească prepared statements lungi, evitând erorile de protocol din modul transaction.
directUrl se conectează direct la baza de date (portul 5432), ocolind PgBouncer. Asta e folosită doar pentru migrări.
Cum calculezi Pool Size?
Nu lăsa valorile default din tutoriale. Regula mea de aur pe care am aplicat-o la proiectul menționat:
- În Postgres (
postgresql.conf), seteazămax_connections = 100(sau cât duce RAM-ul tău, dar nu exagera). - În PgBouncer (
pgbouncer.ini), seteazădefault_pool_size = 20șimax_client_conn = 500. - În conexiunea Prisma din aplicație (în
DATABASE_URL), seteazăconnection_limit=3.
Dacă ai 15 instanțe de app, ele vor cere maxim 45 de conexiuni de la PgBouncer (15 * 3). PgBouncer le va rula pe toate prin cele 20 de conexiuni reale deschise către Postgres. Baza de date respiră ușurată, CPU-ul stă la 15%, iar utilizatorii nu simt nicio latență.
Voi ce setup folosiți pentru scaling pe Postgres? Ați trecut la Supabase/Neon sau tot pe clasicul PgBouncer self-hosted mergeți?