Skip to content

Blocking the event loop since 3.x ? #3777

Open

Description

@vmaurin

Confirm this is an issue with the Python library and not an underlying OpenAI API

  • This is an issue with the Python library

Describe the bug

We run a service using a standard asyncio event loop, running various async code like aiokafka, redis (async) or aiohttp. After upgrading to 3.x version, we notice after a while symptoms that the event loop is blocked:

  • timeout accessing redis
  • timeout with the aiokafka coordinator/heartbeat co-routine

We tried using the default httpx2 client or httpx2 aiohttp client, but it doesn't solve the issue. Reverting to 2.54 solves the issue

Sorry to be so vague, we have nothing in our logs that could give a pointer to a more precise section of the code. We do use chat completion APIs, and structured output with Pydantic

To Reproduce

  1. Update to 3.x
  2. Wait few hours, the CPU usage is raising, symptom of blocked event loop appears

Code snippets

OS

Linux x86

Python version

Python v3.12.3

Library version

OpenAI v3.6.0

Activity

  1. jbeckwith-oai commented on Sep 1, 2026

    @jbeckwith-oai
    Contributor

    Thanks for the report. We tried to reproduce this against the reported version boundary, but we have not been able to trigger a 3.x-specific event-loop blockage yet.

    What we tried:

    • Compared v2.54.0 and v3.6.0 with release-era pinned dependencies.
    • Ran a local, deterministic HTTP/1.1 server so API/network latency was not a variable.
    • Exercised AsyncOpenAI().chat.completions.parse(...) with a reused client under concurrency, using both a small Pydantic response and a roughly 49 KB structured response.
    • Tested v3.6.0 with its default HTTPX2 client, DefaultAioHttpClient(), and the documented injected legacy httpx.AsyncClient() escape hatch.
    • Repeated the main matrix in a Linux/amd64 Python 3.12 container.
    • Measured event-loop lag, CPU time, RSS, and live asyncio task count over thousands of requests.
    • Also stress-tested constructing unclosed clients repeatedly.

    In those runs, reused clients did not accumulate tasks or memory, and v3.6.0 was not slower or more starved than v2.54.0. Repeatedly constructing clients without explicitly closing them did create pressure, but it was worse with v2.54.0, so that did not match this report. We can reproduce bounded loop lag from synchronous JSON/Pydantic parsing under extreme local throughput, but that behavior is not specific to 3.x and did not worsen over time.

    The v3.0 HTTPX2 migration remains the most relevant version-boundary change, but both v3.6.0 transport choices still share the HTTPX2-facing client layer. Could you help us isolate it with the following?

    1. Please share the exact resolved versions, ideally the relevant lines from pip freeze or your lockfile: openai, httpx2, httpcore2, httpx, aiohttp, anyio, jiter, and pydantic, plus your Linux distribution and whether you use uvloop.

    2. Please share a minimized version of client construction and the OpenAI call site. In particular: is one AsyncOpenAI client reused for the process lifetime, or is a new client created per request? How and when is it closed?

    3. Which API path is involved: chat.completions.create, chat.completions.parse, streaming, or a mix? Are Pydantic model classes static or dynamically created? Rough response size, request rate, and peak OpenAI concurrency would help.

    4. Does v3.6.0 still reproduce when you explicitly inject a legacy HTTPX client?

      from typing import Any, cast
      import httpx
      from openai import AsyncOpenAI
      
      client = AsyncOpenAI(http_client=cast(Any, httpx.AsyncClient()))

      If this is healthy while the default HTTPX2 and aiohttp configurations fail, that would strongly isolate the regression to the HTTPX2/common-client layer.

    5. During the high-CPU window, could you capture a 30–60 second CPU profile or stack sample (py-spy top, py-spy record, or py-spy dump) and an asyncio task dump? Please redact request bodies, headers, keys, and customer data. The hottest stack frames are the most important evidence.

    6. Please describe the failure shape: does one process/core reach 100% CPU, does RSS continually increase, how much event-loop lag do you observe, and does the process recover if OpenAI traffic is paused?

    7. Are proxies, custom transports, tracing/instrumentation hooks, TLS settings, or retry overrides involved?

    With either the legacy-HTTPX result or a redacted hot-stack profile, we should be able to reduce this to a reliable reproducer much more quickly.

  2. vmaurin commented on Sep 2, 2026

    @vmaurin
    Author

    Thank for the investigation @jbeckwith-oai

    About testing, it is happening mainly for us on our real production, interacting with the OpenAI standard endpoints. We observe it is "ramping up" over ~12 hours, at first it is barely visible, then it becomes worse and worse. We usually see also in logs "Retry after" so a local, deterministic HTTP/1.1 server might not be an ideal test case. Maybe something is going wrong with the request retry logic and pilling up here.

    Linux is a ubuntu 24.04 in a container. The host machine is an AWS ECS fargate task (hard to know tons of details)
    For uvloop, we have actually a task running with and one running a vanilla asyncio and we see the CPU ramping up in both scenario (so it doesn't look to impact)

      Declared in pyproject.toml (all exact pins)
    
      "pydantic[email]==2.13.5"
      "aiohttp==3.14.3"
      "openai[aiohttp]==3.6.0"
      httpx, httpx2, httpcore2, anyio, jiter are transitive — not declared directly.
    
      Resolved entries
    
      [[package]]
      name = "openai"
      version = "3.6.0"
      source = { registry = "https://pypi.org/simple" }
      dependencies = [
          { name = "anyio" },
          { name = "httpx2" },
          { name = "jiter" },
          { name = "pydantic" },
          { name = "sniffio" },
          { name = "typing-extensions" },
      ]
      sdist = { url = ".../openai-3.6.0.tar.gz", hash = "sha256:18fe3f6e96390ef41ee27b152fc9effefca321c33673bd9b956a572493d3ab9b", size = 1455376, upload-time = "2026-08-28T22:29:18.268Z" }
    
      [package.optional-dependencies]
      aiohttp = [ { name = "aiohttp" } ]     # ← the [aiohttp] extra we enable
    
      [[package]]
      name = "httpx2"
      version = "2.12.0"
      dependencies = [
          { name = "anyio", marker = "sys_platform != 'emscripten'" },
          { name = "httpcore2", marker = "sys_platform != 'emscripten'" },
          { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" },
          { name = "idna" },
          { name = "truststore", marker = "sys_platform != 'emscripten'" },
          { name = "typing-extensions", marker = "python_full_version < '3.13'" },
      ]
      sdist hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf"  # 2026-08-18
    
      [[package]]
      name = "httpcore2"
      version = "2.12.0"
      dependencies = [ { name = "h11" }, { name = "truststore" } ]
      sdist hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648"  # 2026-08-18
    
      [[package]]
      name = "httpx"
      version = "0.28.1"
      dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" } ]
      sdist hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"  # 2024-12-06
    
      [[package]]
      name = "httpcore"
      version = "1.0.9"
      dependencies = [ { name = "certifi" }, { name = "h11" } ]
      sdist hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"  # 2025-04-24
    
      [[package]]
      name = "aiohttp"
      version = "3.14.3"
      dependencies = [ aiohappyeyeballs, aiosignal, attrs, frozenlist, multidict, propcache,
                       typing-extensions (py<3.13), yarl ]
      sdist hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"  # 2026-07-23
    
      [[package]]
      name = "anyio"
      version = "4.14.2"
      dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" } ]
      sdist hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"  # 2026-07-12
    
      [[package]]
      name = "jiter"
      version = "0.16.0"
      # no dependencies
      sdist hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c"  # 2026-06-29
    
      [[package]]
      name = "pydantic"
      version = "2.13.5"
      dependencies = [ annotated-types, pydantic-core, typing-extensions, typing-inspection ]
      sdist hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"  # 2026-08-28
      [package.optional-dependencies]
      email = [ { name = "email-validator" } ]   # ← the [email] extra we enable
    
      [[package]]
      name = "pydantic-core"
      version = "2.46.5"
      dependencies = [ { name = "typing-extensions" } ]
      sdist hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"  # 2026-08-28
    

    We observed the issue with both

            self._client = AsyncOpenAI(api_key=api_key, organization=organization, _enforce_credentials=False)
    

    and

            self._client = AsyncOpenAI(
                api_key=api_key, organization=organization, _enforce_credentials=False, http_client=DefaultAioHttpClient()
            )
    

    We create a single client for the whole process that is reused during the full process timeline

    We are using client.chat.completions.create
    Then we have a mix of no structured output, and structured output.
    For structured output, we create a class per request, with a code like this

        @staticmethod
        def _create_categorization_response_class(labels, field_ids):
            class Categorization(BaseModel, extra="forbid"):
                category: Annotated[str, DynamicStrEnum(labels)]
                confidence: Annotated[str, DynamicStrEnum(CONFIDENCES.keys())]
                field: Annotated[
                    str,
                    DynamicStrEnum(["*", *field_ids]),
                    Field(description="a field id, it could be `*` for the whole content"),
                ]
    
            class CategorizationResponse(BaseModel, extra="forbid"):
                categorizations: list[Categorization]
    
            return CategorizationResponse
    
    

    At peak we are around 700 RPM on OpenAI, but we do have multiple task running, so more realistically, on each task it is probably less than 100 RPM. Output tokens are rather small, Input tokens probably around ~1000 in average (but we have big variations)

    4 and 5, yet to try

    1. We run our process on a single CPU fargate task. At first the CPU usage is normal, and no symptom of blocked event loop. Then the CPU is ramping up and we are seeing more and more sign of blocked event loop
    Image

    (spike are when we release a new version at 10:00 we tried again 3.x with aiohttp and we roll backed at 21:00)

    1. No
  3. Hughhhhcoder commented on Sep 2, 2026

    @Hughhhhcoder
    Contributor

    I ran an isolated check against the current source with the reported dependency versions: httpx2 2.12.0, pydantic 2.13.5, anyio 4.14.2, and aiohttp 3.14.3. The test made 2,000 sequential chat.completions.parse calls, created one new Pydantic response model per call, and returned a 429 with Retry-After on every 11th transport call. The async task count stayed at 1 throughout. RSS rose during the first 500 calls and then stayed at the same high-water mark through 2,000 calls. This does not reproduce a continuing SDK-side retry-task buildup, although a local MockTransport cannot rule out a production transport or server interaction. The current request path closes retryable responses before sleeping. A useful next discriminator would be a redacted profile comparing max_retries=0 with the default and an explicitly injected legacy HTTPX client. I do not have a stable SDK code defect to patch yet.

  4. aktaseren commented on Sep 5, 2026

    @aktaseren

    For anyone affected by this, it looks like there isn’t enough evidence yet to confirm a 3.x-specific event-loop regression.

    As a temporary workaround, it may be worth testing with max_retries=0 and/or the legacy HTTPX client to see whether the behaviour changes. If either makes a difference, sharing the results here would help narrow down the cause. A CPU/asyncio profile during the CPU ramp-up would also be very useful for identifying where the event loop is being blocked.

  5. vmaurin commented on Sep 8, 2026

    @vmaurin
    Author

    We are running again 3.x but with the Legacy HTTPX client (like suggested by @jbeckwith-oai ) and we are not seeing the issue, so it sounds related to HTTPX2/aiohttp layer

  6. vmaurin commented on Sep 9, 2026

    @vmaurin
    Author

    For uvloop, we have actually a task running with and one running a vanilla asyncio and we see the CPU ramping up in both scenario (so it doesn't look to impact)

    I was wrong here, both the observed task are running with uvloop

  7. NateRVance commented on Sep 28, 2026

    @NateRVance

    We're also experiencing this with asyncio (fastapi). Even without any load beyond a healtcheck which performs a model listing, we observe a CPU utilization ramp-up over a few days:

    Image

    The only openai code running during this duration is an async for model in client.models.list() loop every 15 seconds for the health check.

    Reverting to openai<3 resolves this issue. My next debugging step will be to investigate an uvloop alternative.

  8. dajiaohuang commented on Sep 29, 2026

    @dajiaohuang

    Thanks, this narrows the report to a long-lived health check that repeatedly lists models. The earlier local stress runs did not exercise that cadence over multi-day uptime, so they do not rule this case out. If available, could you share one sanitized task or stack sample while CPU is elevated, along with whether the same AsyncOpenAI client is reused for each check? That should help distinguish accumulated SDK transport/retry work from time spent outside the client. I do not have evidence of an SDK root cause yet.

  9. NateRVance commented on Sep 30, 2026

    @NateRVance

    Thanks for the response! This is actually with multiple long-lived AsyncOpenAI clients throughout; we are building a multi-tenant app, where each tenant has its own client configuration (base_url, api_key, and model selection).

    In the above screenshot, we had been following the guide in https://langfuse.com/integrations/frameworks/openai-agents for monitoring our openai-agents system, which included nest_asyncio, and had also used the from langfuse.openai import AsyncOpenAI client. We removed nest_asyncio, used the openai AsyncOpenAI client directly, and have rerun the experiment over the last day, obtaining the same behavior:

    Image

    This is all with python's asyncio; we investigated uvloop but found that it made matters worse.

    Do you have any recommendations for how to obtain the stack sample? I have started logging asyncio.all_tasks(), but these aren't changing over time. My next step will be to log cProfile outputs.

  10. SantoshDhaladhuli commented on Oct 6, 2026

    @SantoshDhaladhuli

    I have faced a similar issue over time when transitioned to openai 3.x

  11. dajiaohuang commented on Oct 7, 2026

    @dajiaohuang

    Thanks for the additional context. For stack sampling while CPU is elevated, I'd recommend:

    • py-spy dump --pid <pid> to get an instant snapshot of all thread stacks (including the event loop thread), which you can trigger manually when you notice CPU ramping
    • py-spy record -o profile.svg --pid <pid> --duration 30 for a 30-second flame graph
    • For asyncio tasks specifically, logging asyncio.all_tasks() is good, but also try calling task.print_stack() on any tasks that look suspicious or are accumulating

    The fact that legacy HTTPX avoids the issue is strong signal it's in the HTTPX2 transport layer. Multiple per-tenant clients could also be a factor if each client accumulates background work — checking whether the number of active tasks correlates with client count would also be useful.

  12. jbeckwith-oai commented on Oct 7, 2026

    @jbeckwith-oai
    Contributor

    Hi folks! Thanks for sticking with this and sharing the extra details. We finally have a local repro that looks like a strong match for what you're seeing, and we've tested an upstream fix that stops it.

    The problem appears to be in the default TLS setup used by HTTPX2. On the affected Linux/OpenSSL combination, truststore configures the same context's trust locations again on each new connection. Those locations accumulate, making certificate verification progressively more expensive. That work happens on the event-loop thread, which explains how CPU can climb and unrelated tasks can fall behind even while asyncio.all_tasks() stays flat.

    We reproduced this on Ubuntu 24.04, Python 3.12.3, OpenSSL 3.0.13, OpenAI 3.6.0, HTTPX2/httpcore2 2.12.0, and truststore 0.10.4. The test uses one reused client making models.list() calls to a local HTTPS server. All requests succeed with certificate and hostname verification enabled. Retries are disabled, and there are no structured outputs or dynamically created models involved. Current SDK source, 3.16.2, also reproduces with the same dependency versions.

    We then tested truststore PR #213 at 058a811b8bbef1fdd7dd65582276c408f5ede276, unchanged:

    Transport Released truststore 0.10.4 Exact upstream PR
    HTTPX2 36.58 → 59.14 ms CPU/request 4.05 → 4.08 ms
    SDK's aiohttp adapter 34.93 → 57.13 ms CPU/request 3.70 → 3.41 ms

    The arrows compare requests 101-200 with 401-500. We force fresh connections to reproduce the accumulation quickly, and the CPU measurements include the local server. These are comparisons, not predictions of production latency.

    For 50 successful requests, certificate-directory opens fell from 1,275 to 50 with the PR. Both transports still rejected untrusted certificates, and all 10 focused upstream tests passed, including threaded-context and custom-CA coverage.

    A couple of details explain why we missed this initially. Plain HTTP tests don't exercise it, and even a simple TLS fixture missed it until we added an intermediate certificate to the chain. A health check every 15 seconds also exceeds HTTPX2's default five-second keepalive expiry, so correctly reusing a client can still produce fresh handshakes. The SDK's aiohttp adapter uses the same truststore context setup, which explains why that switch alone didn't help.

    For Linux environments matching this case, an explicit verified SSL context was stable in our tests:

    import ssl
    from openai import AsyncOpenAI, DefaultAsyncHttpxClient
    
    client = AsyncOpenAI(
        http_client=DefaultAsyncHttpxClient(verify=ssl.create_default_context()),
    )
    # Reuse the client, and await client.close() at application shutdown.

    For the aiohttp adapter, use DefaultAioHttpClient(verify=ssl.create_default_context()) instead. Keep any custom CA configuration your environment requires. Certificate verification stays enabled. Injecting legacy HTTPX is another option, as already confirmed in this thread.

    We've shared the results on the truststore issue and fix PR, and opened HTTPX2 #1252 with the full local reproducer to track the downstream fix.

    We haven't yet confirmed this is the cause in each reporter's production environment. If you're able to share your OpenSSL and truststore versions, then change only the SSL-context configuration and run beyond the usual onset window, that would be a really useful final check. You can get those versions with:

    import ssl
    from importlib.metadata import version
    
    print(ssl.OPENSSL_VERSION)
    print("truststore", version("truststore"))

    Thanks again for the patience here. The health-check report and the legacy HTTPX comparison were particularly helpful in narrowing this down.

  13. vmaurin commented on Oct 14, 2026

    @vmaurin
    Author

    On our side, we indeed use this mix (container over Ubuntu 24.04)

    >>> print(ssl.OPENSSL_VERSION)
    OpenSSL 3.0.13 30 Jan 2024
    >>> print("truststore", version("truststore"))
    truststore 0.10.4
    

    We applied the proposed fix on our production system

    client = AsyncOpenAI(
        http_client=DefaultAsyncHttpxClient(verify=ssl.create_default_context()),
    )

    and we can confirm the CPU is stable 👍

  14. jbeckwith-oai commented on Oct 19, 2026

    @jbeckwith-oai
    Contributor

    Good progress here! We've now tested the HTTPX2-side fix in pydantic/httpx2#1253, and it stops the CPU ramp in our Linux reproducer for both HTTPX2 and the SDK's aiohttp adapter.

    @vmaurin, thank you for confirming that the SSL-context workaround also keeps CPU stable in your production setup. That is really helpful confirmation.

    We tested the exact PR commit 5606fb4 with SDK 3.6.0, truststore 0.10.4, and OpenSSL 3.0.22. Each run made 500 successful models.list() calls through one reused client to a local HTTPS server with an intermediate certificate chain, forcing fresh connections.

    CPU time per request, comparing calls 101–200 with 401–500:

    Transport Before the patch With the patch
    HTTPX2 49.25 → 95.63 ms 4.30 → 4.26 ms
    SDK aiohttp adapter 43.02 → 67.96 ms 3.24 → 3.24 ms

    Certificate verification stayed enabled. Both patched transports rejected untrusted certificates, hostname verification still worked, and all 29 upstream configuration tests passed. The full verification details are here.

    The HTTPX2 PR is still open. For affected Linux setups, the explicit ssl.create_default_context() workaround in the earlier comment remains an option while the dependency fix is pending. Please preserve any custom CA configuration your environment needs.

    Huge thanks to @Kludex for putting this fix together, and to everyone here for the reports, testing, and patience. We really appreciate it!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions