Cause #1: Code That Doesn't Close Connections
This is the number one reason. I've seen it in PHP, Node.js, Python — doesn't matter the language. Someone writes code that opens a connection to the database but never closes it. Every time that code runs, a new connection stays open. After a few hundred requests, your database hits max_connections and dies.
Last month I had a client running a small e-commerce site on a shared server. Every time a customer added an item to cart, their PHP script opened a MySQL connection but forgot to close it. Within 15 minutes of a sale, the whole site went down. Fix was simple — close the connection after the query.
The Fix
For PHP with PDO:
$pdo = new PDO('mysql:host=localhost;dbname=shop', 'user', 'pass');
$stmt = $pdo->query('SELECT * FROM products');
$results = $stmt->fetchAll();
$pdo = null; // close it manually
For Node.js with mysql2:
const conn = await mysql.createConnection({...});
const [rows] = await conn.query('SELECT * FROM products');
await conn.end();
If you're using an ORM like Sequelize or Doctrine, make sure you're not opening connections outside the pool. ORMs often cache connections, but if you call new Sequelize() inside a loop without closing, you'll leak.
Also check your error handlers. If a query throws an exception and you don't close the connection in a finally block, that connection stays open. Use try/catch/finally every time.
Cause #2: Connection Pool Set Too Big
This one gets people who think "more connections = faster" — nope. A pool of 50 connections might look good, but if your database only allows 100 total connections and you have 3 app servers each with a pool of 50, you're already at 150. Add a few leaked connections, and boom — too many connections error.
I fixed a setup at a logistics company last year. They had a Java app with a HikariCP pool set to 100. Their MySQL max_connections was 150. Two instances of the app ran on different servers. That's 200 connections when the DB could only handle 150. The result? Random crashes every few hours.
The Fix
Set your pool size based on your database's max_connections and the number of app instances. A good rule of thumb: take max_connections, subtract 10 for admin tasks, then divide by the number of app servers. So if max_connections is 150 and you have 3 servers, each pool should be no more than (150 - 10) / 3 = ~46 connections per server.
For MySQL check current limit:
SHOW VARIABLES LIKE 'max_connections';
Increase it if needed (but don't go crazy — each connection uses RAM):
SET GLOBAL max_connections = 200;
Also set connection timeout in your pool config. In HikariCP:
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(20);
config.setConnectionTimeout(30000); // 30 seconds
config.setIdleTimeout(600000); // 10 minutes idle
For pgBouncer or PgPool, same logic applies — keep pool sizes small and match them to your workload.
Cause #3: Connections Not Timing Out
Even if you close connections in code, sometimes a connection hangs because of a slow query, network issue, or a transaction that never commits. If your database doesn't kill these idle connections, they pile up over hours or days.
Had a client whose CRM ran fine for 3 days, then on Thursday afternoons it always crashed. Turned out a reporting script ran a 5-minute query every hour. Each query left a connection in 'sleep' state because the script exited without closing. After 72 hours, they had 72 sleeping connections eating up the pool.
The Fix
Set a wait_timeout on the database server. For MySQL:
SET GLOBAL wait_timeout = 300; // 5 minutes
SET GLOBAL interactive_timeout = 300;
Make these permanent in my.cnf:
[mysqld]
wait_timeout = 300
interactive_timeout = 300
max_connections = 150
For Postgres, set in postgresql.conf:
idle_in_transaction_session_timeout = '5min'
statement_timeout = '30s'
Also monitor active connections to see who's hanging. Check MySQL with:
SHOW FULL PROCESSLIST;
Look for connections that have been in 'Sleep' state for more than a few seconds. Kill them manually if needed:
KILL CONNECTION 12345;
But better to automate with a cron job that runs every 5 minutes and kills connections older than a threshold. Here's a quick bash script for MySQL:
#!/bin/bash
mysql -e "SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist WHERE command = 'Sleep' AND time > 300" | tail -n +2 | mysql
Quick-Reference Summary Table
| Cause | Symptom | Fix |
|---|---|---|
| Code doesn't close connections | Connections pile up fast during traffic | Close connections after queries, use try/finally |
| Pool size too big | Multiple app servers each grab too many | Set pool to (max_connections - 10) / app servers |
| No timeout on idle connections | Sleeping connections accumulate over hours | Set wait_timeout / idle_in_transaction_session_timeout |
Bottom line: connection leaks happen because someone forgot to close a connection or didn't set limits. Check your code first, then your pool settings, then your database timeouts. That order fixes 95% of cases.