CLOUD
development

Database Connection Pooling: Why Your Database Collapses at 200 Connections, and the Fix

CLOUD|September 20, 2026|7 min read|143 views
Database Connection Pooling: Why Your Database Collapses at 200 Connections, and the Fix

Every new database connection costs memory and milliseconds to negotiate, and at hundreds of connections it devours memory and collapses. The fix is connection pooling: a small reused pool, and a proxy like PgBouncer when needed.

Your database does not collapse because it processes many queries, it usually collapses because it opens too many connections at once. Every new connection is not free: opening it requires a network handshake, then authentication, then memory allocation inside the database, all of which costs 1 to 3 milliseconds before a single query runs. Worse, every open connection reserves lasting memory: in MySQL each connection thread typically reserves 256 KB to several MB depending on per-session buffer settings, and in PostgreSQL each connection is a separate process costing around 5 to 10 MB. Multiply that by 200 concurrent connections and PostgreSQL alone has consumed 1 to 2 GB of memory on connections only, before any data. The direct answer: do not open a new connection per request, use connection pooling, a small pool of ready connections that get reused. Raising max_connections alone is a wrong fix because it postpones the collapse to a bigger number then makes it worse when it happens, since memory runs out faster. The correct pool is deliberately small, and a well-known practical rule is that the optimal connection count is roughly (cores x 2) plus the number of disks, so a 4-core server may be faster with 10 active connections than with 200.

The core rule: connections are a scarce, precious resource, not cheap or infinite. A small reused pool is faster and more stable than a flood of connections competing for a limited CPU.

Why a new connection is expensive

When your app requests a new connection, a chain of steps runs before the first query:

  • TCP handshake: packet exchange to establish the channel.
  • Authentication: exchanging and verifying credentials.
  • Resource allocation: the database creates a thread or process and reserves memory buffers for it.

All of this is 1 to 3 milliseconds per connection. If your app opens and closes a connection per HTTP request, you pay this tax thousands of times a minute. A pool pays it once at app startup, then each request borrows a ready connection from the pool, runs its query immediately, and returns it, so the time to acquire a connection drops from milliseconds to microseconds.

Why raising max_connections alone is wrong

The instinctive fix for a "too many connections" error is raising max_connections from 151 (the MySQL default) to 500 or 1000. This treats the symptom and worsens the disease. The reason is that every allowed connection reserves latent memory, so you raise the memory ceiling beyond the server's own memory, and when connections actually fill up the server collapses entirely (OOM kill) instead of politely refusing one connection.

AspectRaising max_connectionsConnection pooling
Memory consumptionRises linearly with the ceilingFixed, bounded by pool size
CPU contentionGrows, each connection competesControlled by a small active count
Behavior at peakSudden crash when memory runs outRequests wait politely in the pool queue
Time to acquire a connectionMilliseconds (new connection)Microseconds (reused connection)

The right approach is to keep max_connections reasonable for your server's memory and place a pool in front of it that manages requests with a small connection count.

The pool inside the app: Laravel and Node

Modern frameworks pool connections, but the default may not fit your load. Tune it deliberately:

Laravel and PHP

The PHP-FPM model opens a connection per worker process, so the connection count is close to the FPM worker count. If you have 100 FPM workers on each app server and three servers, you are at 300 potential connections. The correct tuning: reduce FPM workers to what your cores actually fit, and for serverless platforms or many workers use an external proxy (coming next). PHP by nature does not keep a persistent pool between requests except through persistent connections, which need care.

Node.js

Libraries like pg and mysql2 give you an explicit pool. Set max (the pool's maximum size) carefully, since a reasonable value per app instance is often 10 to 20 connections, not 100. Remember that several Node instances multiply this number by their count, so compute the total against the database ceiling. Also set the idle timeout to close excess connections, and the connection timeout so a request fails fast instead of hanging.

PgBouncer and ProxySQL: when you need an external proxy

When app instances multiply (many servers, containers, serverless functions), each internal pool becomes an island unaware of the others, so the total connections on the database balloon. This is where a central pooling proxy comes in:

  • PgBouncer (for PostgreSQL): very lightweight, sits between your apps and the database, and pools thousands of app connections into dozens of real database connections. In transaction pooling mode it reuses the connection between transactions, serving a huge number of clients with tiny resources.
  • ProxySQL (for MySQL): pools connections and adds query routing, read distribution across replicas, and result caching.

The practical rule: if your app runs on one or two servers with tuned internal pools, you usually do not need an external proxy. You need it when the number of app instances exceeds the internal pools' ability to stay under the database ceiling, or with serverless where connections spawn without limit. Running PgBouncer on the same cloud server is cheap because it barely consumes resources.

How many connections your plan handles

Because each connection reserves memory, the safe connection count ties to your server's memory. A practical estimate on cloud server plans:

PlanRAMReasonable pool size
Asas2 GB10 to 20 active connections
Numuw4 GB20 to 40 connections
Aamaal8 GB40 to 80 connections, or PgBouncer for more
Muassasa16 GBWider pools with room for caching

Notice these numbers are far smaller than many expect, and that is intentional: the database performs better with a small active connection count than a large competing one. If you need wider memory for the database, adding +2GB RAM for 600 SAR per year is a direct fix.

The three pooling modes: know the difference before you choose

When you use a proxy like PgBouncer, it offers three pooling modes that differ fundamentally in efficiency and constraints, and a wrong choice breaks your app silently:

ModeWhen the connection returns to the poolEfficiency
session poolingWhen the client's whole session endsLowest, a connection is held for the client's whole session
transaction poolingWhen each transaction endsHighest, most used for the web
statement poolingWhen each statement endsMaximum, but with strict constraints

The most common and efficient mode for web apps is transaction pooling, because each HTTP request is usually a short transaction, so the connection returns immediately afterward for another client, and a small number of real connections serves thousands of clients. But note the important constraint: in transaction pooling you cannot rely on state that lives across transactions such as session-level prepared statements, temporary variables (SET), or temporary tables, because the connection may go to another client between transactions. So configure your driver to avoid persistent prepared statements, or use a pool-compatible setting. The session pooling mode is safer for compatibility but wastes the benefit because it holds the connection for the client's whole session, so use it only if your app truly depends on session state.

Avoid the choking queue

A small pool means requests wait their turn when it fills, which is intentional and healthy, but make sure the wait does not grow until the request times out. Monitor two metrics: queue wait time (how long a request spends waiting for a connection) and pool saturation. If you see frequent waiting, the fix is not always a bigger pool, it is often speeding up the queries themselves so the connection returns faster, since a slow query holding a connection for a long time is the pool's real enemy.

Practical conclusion

Start by measuring: monitor the actual concurrent connection count (SHOW STATUS LIKE 'Threads_connected' in MySQL, or pg_stat_activity in PostgreSQL). If you see tens or hundreds of idle connections, you are wasting memory. Set the app pool size to a small number based on the (cores x 2) rule, not on hope. Keep max_connections reasonable for your memory and do not raise it as a symptom fix. If your app instances multiply or you use serverless, place PgBouncer or ProxySQL as a central pool. The final rule: a connection is a scarce resource you manage carefully, not a counter you raise until the server collapses. And if you want a server with memory enough for a comfortable pool, start from Riyadh servers with a response time near 8ms.

Share