# pgbouncer.ini
[databases]
# Conexiunea către baza de date reală
my_db = host=db.prod.internal port=5432 dbname=production
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# Modul de pooling - CRUCIAL pentru scalare
pool_mode = transaction
# Limitări conexiuni
max_client_conn = 1000
default_pool_size = 15
# Compatibilitate Prisma (opțional, dar recomandat ca fallback)
ignore_startup_parameters = extra_float_digitsDacă ai rulat vreodată Postgres în producție cu Node.js, sigur te-ai lovit de faimoasa eroare „too many clients already”. Postgres deschide un proces separat pentru fiecare conexiune, ceea ce mănâncă RAM de rupe. La un proiect cu peste 15 microservicii în AWS ECS și spike-uri de 10k useri concurenți, baza noastră de date pur și simplu îngenunchea din cauza overhead-ului de conexiuni înainte să punem PgBouncer în față.
PgBouncer este un connection pooler extrem de lightweight. Consumă infim de puține resurse și știe să țină mii de conexiuni deschise dinspre clienți, rutându-le inteligent către doar câteva zeci de conexiuni reale în baza de date.
Session vs Transaction Mode. Care-i faza?
Aici e marea capcană unde mulți își prind degetele. PgBouncer are trei moduri, dar în producție discuția se poartă între două:
- Session Mode: PgBouncer îi alocă clientului o conexiune fizică la Postgres din momentul în care s-a conectat și o ține blocată până când clientul închide sesiunea. Este cel mai sigur mod, dar e aproape inutil dacă ai arhitecturi serverless sau zeci de containere Docker care stau constant pornite. Nu rezolvă problema scalării.
- Transaction Mode: Aici se întâmplă magia. PgBouncer dă conexiunea fizică înapoi în pool imediat ce tranzacția SQL s-a terminat. Următorul query din aceeași sesiune a aplicației poate rula pe o altă conexiune fizică.
Trade-off-ul sincer: Transaction mode îți oferă o scalabilitate brutală, dar vine cu penalizări masive dacă nu ești atent. Pierzi complet suportul pentru tabele temporare (session-level), advisory locks și LISTEN/NOTIFY. De asemenea, prepared statements devin o problemă uriașă pentru că PgBouncer nu garantează că următorul query va nimeri pe conexiunea unde s-a făcut PREPARE.
Integrarea cu Prisma (și de ce crapă default)
Prisma folosește prepared statements la greu pentru optimizare. Dacă legi Prisma direct la PgBouncer în mod transaction, o să te trezești instant în loguri cu erori de genul prepared statement "s0" already exists sau query-uri eșuate aiurea.
Ca să funcționeze corect, trebuie să configurezi două lucruri în connection string-ul de Prisma:
- Adaugi parametrul
&pgbouncer=trueîn URL-ul bazei de date. Asta îi spune motorului de Prisma să folosească protocoale speciale compatibile cu poolerele de conexiuni (practic dezactivează prepared statements pe acea conexiune). - Păstrezi o conexiune directă (fără PgBouncer) doar pentru migrații. Prisma are nevoie de acces direct la Postgres (de regulă portul 5432) când rulezi
prisma migrate deploy, deoarece migrațiile folosesc lock-uri la nivel de sesiune pe care PgBouncer le-ar bloca în mod transaction.
Cum calculăm Pool Sizes în producție?
Nu pune numere din burtă. Regula mea empirică pentru calculul default_pool_size în PgBouncer este:
Pool Size = ((vCPUs * 2) + Effective Spindle Count)
Dacă ai o instanță RDS cu 4 vCPUs și stocare SSD rapidă, un pool de 15-20 de conexiuni reale către Postgres este mai mult decât suficient pentru a satura CPU-ul la maximum în mod eficient. În PgBouncer setezi max_client_conn la 1000 (câte conexiuni acceptă de la aplicații) și default_pool_size la 15 (câte conexiuni deschide el efectiv spre Postgres).
În cazul nostru, făcând această trecere și limitând conexiunile active reale la doar 20, am redus utilizarea CPU pe baza de date de la 85% la un stabil 45% în orele de vârf, eliminând complet timeout-urile de conexiune.
Voi cum gestionați asta în producție? Mergeți pe instanțe dedicate de PgBouncer rulate de voi sau folosiți soluțiile integrate din Supabase ori AWS RDS Proxy?