Skip to content

Bug: httpx.AsyncClient (and sync Client) in Python SDK never closed — connection pool leaks on sandbox kill #1155

@marlonbarreto-git

Description

@marlonbarreto-git

Description

When an AsyncSandbox or Sandbox instance is created, an httpx.AsyncClient (self._envd_api) is instantiated with its own connection pool. When the sandbox is killed (via kill(), __aexit__, or __exit__), the httpx client is never closed. This leaks TCP connections and file descriptors.

Root Cause

Creation (sandbox_async/main.py, lines 96-102):

self._envd_api = httpx.AsyncClient(
    base_url=self.connection_config.get_sandbox_url(
        self.sandbox_id, self.sandbox_domain
    ),
    transport=self._transport,
    headers=self.connection_config.sandbox_headers,
)

__aexit__ (lines 315-316):

async def __aexit__(self, exc_type, exc_value, traceback):
    await self.kill()  # only calls API to kill remote sandbox

kill() (lines 346-358) delegates to SandboxApi._cls_kill(), which is a static API call — it never touches self._envd_api.

No cleanup anywhere: Searching the entire Python SDK for _envd_api.aclose, _envd_api.close, _transport.aclose, _transport.close returns zero results.

The same issue exists in the sync SDK (sandbox_sync/main.py).

Impact

flowchart LR
    A[Sandbox.create] --> B[httpx.AsyncClient created]
    B --> C[sandbox.kill / __aexit__]
    C --> D[API call to kill remote sandbox]
    C -.->|MISSING| E[await _envd_api.aclose]
    D --> F[Client abandoned with open connections]
    F --> G[TCP sockets + FDs leak]
Loading
  • File descriptor exhaustion: Each unclosed httpx.AsyncClient holds open sockets. In long-running applications creating many sandboxes (the primary use case for AI agent orchestrators), this leads to OSError: [Errno 24] Too many open files
  • ResourceWarning: Python emits ResourceWarning: unclosed on garbage collection
  • Connection pool bloat: httpx maintains a connection pool per client; unclosed clients keep pools alive

Reproduction

import asyncio
from e2b import AsyncSandbox

async def leak():
    for i in range(100):
        async with AsyncSandbox.create() as sandbox:
            await sandbox.commands.run("echo hello")
        # After each iteration: httpx client leaked
        # After 100 iterations: 100 unclosed clients
    # Eventually: OSError: Too many open files

asyncio.run(leak())

Proposed Fix

Add cleanup in __aexit__ (and __exit__ for sync):

async def __aexit__(self, exc_type, exc_value, traceback):
    await self.kill()
    await self._envd_api.aclose()

And for the sync SDK:

def __exit__(self, exc_type, exc_value, traceback):
    self.kill()
    self._envd_api.close()

Consider also adding a close() method for users who don't use the context manager.

Current Proposed
Resource cleanup None Proper httpx client shutdown
Breaking changes None None
Complexity N/A +1 line per SDK variant

Environment

  • Python SDK: latest
  • Files: sandbox_async/main.py, sandbox_sync/main.py

I'm happy to open a PR for this fix if you'd like.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions