DEV Community

Aji K
Aji K

Posted on • Edited on • Originally published at Medium

The Most Expensive Line of Python Code Is Often the One Nobody Notices

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.

DB connection
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()

Enter fullscreen mode Exit fullscreen mode

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()

Enter fullscreen mode Exit fullscreen mode

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.

Multiple Connections

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.

Taxi Analogy

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",
)

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

Now your application code becomes cleaner.


with db_connection() as conn:

    cursor = conn.cursor()

    cursor.execute(
        """
        SELECT *
        FROM products
        """
    )

    products = cursor.fetchall()

Enter fullscreen mode Exit fullscreen mode

Even if an exception occurs, the connection is safely returned to the pool.

Context Manager

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)