How a simple database connection can quietly become your application’s biggest scalability bottleneck — and how connection pooling solves it.
It Started With a Simple Question…
Imagine you’re staying in a gated apartment complex.
One morning, you call a plumber to fix a leaking tap.
The security guard checks his ID.
He signs the visitor register.
He receives a visitor pass.
He walks to your apartment.
He tightens one loose pipe.
Five minutes later, he leaves.
Everything seems perfectly normal.
Now imagine your apartment has 100 leaking taps.
Instead of asking the plumber to fix all 100 taps during the same visit, you ask him to leave after fixing each one.
For every single tap, he has to:
Stop at the security gate
Show his ID again
Sign the visitor register
Collect a new visitor pass
Walk to your apartment
Fix one tap
Leave
Repeat this process one hundred times.
Eventually, the plumber spends far more time entering and leaving the building than actually fixing taps.
If you think that’s inefficient, congratulations — you already understand one of the most common performance problems in backend applications.
A database connection works in almost exactly the same way.

We Usually Blame the Wrong Thing
When an application becomes slow, our first instinct is often to suspect:
Slow SQL queries
Missing database indexes
High CPU usage
Memory pressure
Network latency
These are all valid possibilities.
But there’s another bottleneck that quietly grows in the background.
The application may simply be spending too much time creating database connections.
Every new connection requires work before the first SQL statement even executes.
A Simple Python Example
Let’s look at code that appears perfectly reasonable.
import psycopg2
def get_customer(customer_id: int):
conn = psycopg2.connect(
host="localhost",
database="shop",
user="postgres",
password="password"
)
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT *
FROM customers
WHERE id = %s
""",
(customer_id,)
)
return cursor.fetchone()
finally:
conn.close()
There’s nothing technically wrong with this code.
In fact, if you’re learning Python or PostgreSQL, it’s exactly the kind of example you’ll see in many tutorials.
So what’s the issue?
Imagine this function is called:
500 times every minute
by 2,000 concurrent users
across four application instances
Your database is now creating — and tearing down — thousands of connections every minute.
Most of that work contributes nothing to your business logic.
It’s simply overhead.
The Problem Becomes Worse Inside Loops
Here’s another example that looks harmless.
for order in orders:
conn = psycopg2.connect(...)
cursor = conn.cursor()
cursor.execute(
"""
INSERT INTO orders(...)
VALUES(...)
"""
)
conn.commit()
conn.close()
Many developers write code like this during early development.
It works.
It passes testing.
But under production load, it can become extremely expensive.
Instead of reusing an existing connection, the application repeatedly creates new ones.
Enter Connection Pooling
Instead of creating a brand-new connection every time we need the database…
Why not keep a few connections ready?
That’s exactly what connection pooling does.
Think of it like a fleet of taxis.
Passengers don’t buy a new taxi every morning.
They simply hire one.
Complete the journey.
The taxi returns to the queue.
The next passenger uses the same taxi.
Database connections should work the same way.
Borrow.
Use.
Return.
Reuse.
Creating Your First Connection Pool
Using psycopg2, creating a connection pool is surprisingly straightforward.
from psycopg2.pool import SimpleConnectionPool
pool = SimpleConnectionPool(
minconn=2,
maxconn=10,
host="localhost",
database="shop",
user="postgres",
password="password",
)
Instead of opening a new connection every time, we now borrow one from the pool.
conn = pool.getconn()
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT *
FROM customers
"""
)
customers = cursor.fetchall()
finally:
pool.putconn(conn)
Notice something important.
We don’t call conn.close().
Instead, we return the connection to the pool.
The next request can reuse it immediately.
A More Pythonic Approach: Context Managers
One of the easiest mistakes developers make is forgetting to return a connection.
Python’s context managers provide an elegant solution.
from contextlib import contextmanager
@contextmanager
def db_connection():
conn = pool.getconn()
try:
yield conn
finally:
pool.putconn(conn)
Now your application code becomes cleaner.
with db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT *
FROM products
"""
)
products = cursor.fetchall()
Even if an exception occurs, the connection is safely returned to the pool.
One lesson I’ve learned while working on backend systems is that the biggest performance improvements often come from eliminating unnecessary work rather than making existing work faster. Reusing a database connection is one of those seemingly small decisions that can have a significant impact on scalability and reliability.
If this article helped you see connection pooling from a different perspective, consider giving it a clap — it helps more developers discover it. I write about Python, AWS, backend architecture, distributed systems, and the engineering decisions that shape reliable production software. If those topics interest you, feel free to follow my profile. And if you have a different approach or an interesting experience to share, I’d genuinely enjoy continuing the discussion in the comments.



Top comments (0)