Skip to content

Tracing: the default BackendSpanExporter's HTTP client is never closed #4681

Closed

Description

@rajarshidattapy

Area: src/agents/tracing/processors.py:533, src/agents/tracing/provider.py:506, src/agents/tracing/setup.py
Type: bug / resource leak
Severity: low-medium

Description

BackendSpanExporter opens a pooled HTTP client in its constructor and keeps it for the
process lifetime:

# Keep a client open for connection pooling across multiple export calls
self._client = httpx2.Client(timeout=httpx2.Timeout(timeout=60, connect=5.0))

It exposes a matching close():

def close(self):
    """Close the underlying HTTP client."""
    self._client.close()

but nothing in the shutdown chain ever calls it. The chain is:

atexit → _shutdown_global_trace_provider() → DefaultTraceProvider.shutdown() →
MultiTracingProcessor.shutdown() → BatchTraceProcessor.shutdown()

BatchTraceProcessor.shutdown() sets its shutdown event, optionally calls the exporter's
private _request_shutdown(), and joins the worker thread — it never calls
self._exporter.close(). Grepping the repo, the only callers of exporter.close() are in
tests/test_trace_processor.py.

Repro

from agents.tracing.processors import BackendSpanExporter, BatchTraceProcessor

exporter = BackendSpanExporter(api_key="test_key")
processor = BatchTraceProcessor(exporter=exporter)
processor.shutdown()
print("client closed after processor shutdown:", exporter._client.is_closed)  # False

Impact

  • The TLS connection(s) to api.openai.com are left open until the interpreter tears the
    socket down, producing ResourceWarning: unclosed <ssl.SSLSocket ...> under
    -W error::ResourceWarning or PYTHONDEVMODE=1 (this breaks strict CI setups that treat
    ResourceWarnings as errors).
  • Applications that replace the global trace provider / processor at runtime (multi-tenant
    servers switching export API keys, test suites that build a fresh
    BatchTraceProcessor(BackendSpanExporter(...)) per case) leak one client and its
    connection pool per replacement.

Suggested fix

Have BatchTraceProcessor.shutdown() call close() on its exporter when the exporter
exposes one, mirroring the existing _request_shutdown duck-typing:

close = getattr(self._exporter, "close", None)
if callable(close):
    close()

after the final flush, so the exporter still has its client while queued spans are drained.

Activity

  1. mikemikimike commented on Aug 27, 2026

    @mikemikimike

    Implemented in PR #4710.

    • Changed: Close the lazily-created default tracing exporter at the setup/provider shutdown boundary after the final flush, while leaving injected exporters under caller ownership.
    • Tests: The tracing-focused suites pass (159 tests), with Ruff format/check, mypy, and git diff --check also passing.
    • PR: fix(tracing): close the default exporter at shutdown #4710
  2. seratch commented on Aug 31, 2026

    @seratch
    Member

    Closing this issue for the reason I stated at #4737 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions