A couple of years ago, one of our tech leads asked me to set up PgBouncer for a project we were working on.
At the time, I was still pretty junior. I followed the documentation, used ChatGPT when I got stuck, got it working, and moved on.
But if you asked me why we needed it — I had no answer.
We already had pooling set up:
const pool = new Pool({ max: 20 });
App was working. Postgres was working. Users were happy. So why add another component on top?
I just assumed it was one of those "senior engineer things" and didn't question it.
I started going deeper on topics like autoscaling, Kubernetes, how apps behave under real load.
That's when the math hit me.
Every Node.js instance creates its own pool Independently With no awareness of the others.
| Instances | Max Connections | Direct DB Connections |
|---|---|---|
| 1 | 20 | 20 |
| 10 | 20 | 200 |
| 30 | 20 | 600 |
All of those go directly to Postgres. 600 separate backend processes. 600 memory allocations. Postgres managing all of that before it even runs a single query.
That's when the PgBouncer setup from a year ago finally made sense.
Instead of every instance talking directly to Postgres, they all talk to PgBouncer. PgBouncer maintains a small number of real connections and multiplexes everything through them.
Instance 1 ──┐
Instance 2 ──┤
Instance 3 ──┼──▶ PgBouncer ──▶ PostgreSQL
... │
Instance 30 ──┘
600 logical connections → 25 actual ones.
Postgres stops managing connections and starts running queries.
Some of them don't make sense when you implement them.
They make sense six months later, after you've learned a few more pieces of the puzzle.
The senior who asked me to set up PgBouncer wasn't solving a problem we had. He was preventing one we hadn't hit yet. That's a different kind of thinking and honestly it took me a while to get there.
More detailed blog here 👉 PostgreSQL Connection Pooling Explained — 21 min read
No responses yet.