Tech Verse Logo
Enable dark mode
Fixing Async Python Memory Leaks from Unclosed Sessions

Fixing Async Python Memory Leaks from Unclosed Sessions

Md. Mostafijur RahmanMMd. Mostafijur Rahman

Md. Mostafijur Rahman

•4 min read

Memory management in async Python looks straightforward until a container gets OOM-killed in production. You write an async function, you fetch some data via HTTP or query a database, and you move on. The event loop handles the scheduling, but it doesn't clean up your connections. If you don't explicitly close your network sessions and database pools, they accumulate silently. You'll notice RSS memory climbing day by day until the orchestrator steps in and restarts the pod.

The issue usually stems from a misunderstanding of how connection lifecycles work inside event loops. When you instantiate a client without context managers, you're tying its lifetime to manual teardown calls that rarely happen correctly under error conditions. Let's look at how httpx and asyncpg handle this, and how you can restructure your code to stop the bleed. For a broader look at organizing asynchronous control flows, check out Async Python: asyncio Without the Confusion to see how tasks and event loops interact under the hood.

The Httpx Client Lifecycle Gotcha

Creating a new httpx.AsyncClient inside a frequently called request handler is a fast track to resource exhaustion. Every time you construct a client, it spins up an underlying connection pool and transport mechanism. If you don't use it as an async context manager, those underlying sockets linger in a close-wait state or stay attached to the event loop's internal structures. People often instantiate the client globally or inside route handlers without realizing that connection pools need explicit disposal. When an exception occurs mid-request, the cleanup code gets skipped entirely unless you rely on language-level guarantees.

You might think that garbage collection will eventually scoop up the unreferenced client object and close its sockets. Python's garbage collector deals with reference cycles, but socket cleanup relies on finalizers that don't run immediately. Under high concurrency, connections arrive faster than the garbage collector can run, leading to exhausted file descriptors and ballooning memory usage. If you want to profile your code to see where objects linger before fixing them, take a look at Profiling Python: Finding the Actual Bottleneck for practical ways to trace object allocations.

Here is what an incorrect client setup looks like:

import httpx

async def fetch_remote_data(url: str) -> dict:
    client = httpx.AsyncClient()
    response = await client.get(url)
    return response.json()

That snippet creates a fresh client on every invocation without closing it. The fix is straightforward. Wrap the client in an async context manager, or maintain a single application-scoped client instance that lives for the entire lifecycle of the service.

Asyncpg Pool Management Failures

Database connections managed by asyncpg present similar risks on a larger scale. If you open a new connection or pool inside an endpoint handler rather than initializing it at startup, you will leak pool structures and backend server processes. Each pool holds a configured minimum number of connections, and if your application spawns pools dynamically per request, your database server will quickly run out of available backends.

Applications often create connection pools inside request middleware or startup events without ensuring they shut down during application teardown. When tests run or development servers reload, these dangling pools throw warnings or leave ghost connections open on the database server. You need to tie the pool lifecycle directly to your application's startup and shutdown events.

Here is a correct implementation of pool management using a module-level variable:

import asyncpg

class Database:
    pool: asyncpg.Pool | None = None

    @classmethod
    async def connect(cls, dsn: str):
        cls.pool = await asyncpg.create_pool(dsn)

    @classmethod
    async def disconnect(cls):
        if cls.pool:
            await cls.pool.close()

db = Database()

During your framework's startup phase, you call await db.connect(dsn). During shutdown, you call await db.disconnect(). If you skip the disconnect call, the event loop terminates while background tasks inside the pool are still waiting on socket reads, leaving orphaned connections hanging on your database instance.

Diagnosing Leaks in Containerized Deployments

Finding these leaks requires looking beyond standard application logs. When a container's memory usage steps up in a staircase pattern and never drops back down, you are dealing with a resource leak rather than a traffic spike. You can use tools inside the container to inspect open file descriptors and verify whether your async clients are leaking sockets. Running command-line diagnostics helps pinpoint the exact socket states.

You can check the open file descriptors of a running Python process by inspecting the /proc filesystem on Linux systems. If you see thousands of sockets in CLOSE_WAIT or TIME_WAIT tied to your Python process ID, your async HTTP clients or database pools are not being closed properly. Combining process inspection with proper linting rules or type checkers like the ones discussed in Python Type Hints and Mypy: Real World Patterns helps ensure that resource-handling functions are typed and used correctly across your codebase.

Md. Mostafijur RahmanMMd. Mostafijur Rahman

WRITTEN BY

Md. Mostafijur Rahman

    Latest Posts

    View All

    Local Embedding Models in Production RAG

    Local Embedding Models in Production RAG

    Preventing nextjs middleware cache poisoning

    Preventing nextjs middleware cache poisoning

    Fixing Async Python Memory Leaks from Unclosed Sessions

    Fixing Async Python Memory Leaks from Unclosed Sessions

    Optimizing Inertia SSR Performance in Production

    Optimizing Inertia SSR Performance in Production

    Cron vs Systemd Timers: Which to Use

    Cron vs Systemd Timers: Which to Use

    Configuring Linux Deploy Users, Sudo, and SSH Keys

    Configuring Linux Deploy Users, Sudo, and SSH Keys

    Linux Disk Full Troubleshooting

    Linux Disk Full Troubleshooting

    Nginx Reverse Proxy for Node and PHP

    Nginx Reverse Proxy for Node and PHP

    Mastering Linux File Permissions and Web App Storage

    Mastering Linux File Permissions and Web App Storage

    Self-Hosted Linux Server Monitoring Without SaaS Fees

    Self-Hosted Linux Server Monitoring Without SaaS Fees