Your service has 500 threads. Somebody sets the connection pool to 500 to match, and the database falls over.
In Postgres every connection is a separate operating system process. Several megabytes of memory before it does any work, and one more thing the scheduler has to juggle. Five hundred of them on a sixteen-core machine means five hundred processes taking turns at sixteen seats. Context switching climbs, memory climbs, and throughput goes down rather than up.
The queue never disappears. You only choose where it forms. With a small pool it forms in your application, which is cheap — the database runs at full speed and performance stays predictable. With a large pool it forms inside the database, where connections contend for the same cores and the database itself becomes the bottleneck.
A pooler like PgBouncer lets thousands of client connections share a small number of real ones. Start in the low tens, keep transactions short, and never hold a connection while waiting on an external call.
When something is scarce, the answer is rarely to ask for more of it. It's to hold it for less time.
