You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Update to 3.x
Wait few hours, the CPU usage is raising, symptom of blocked event loop appears
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?
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.
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?
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.
Does v3.6.0 still reproduce when you explicitly inject a legacy HTTPX client?
If this is healthy while the default HTTPX2 and aiohttp configurations fail, that would strongly isolate the regression to the HTTPX2/common-client layer.
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.
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?
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.
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 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
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
(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)
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.
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.
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
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
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:
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.
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.
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:
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.
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.
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:
importsslfromopenaiimportAsyncOpenAI, DefaultAsyncHttpxClientclient=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:
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!
Activity
jbeckwith-oai commentedon Sep 1, 2026
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:
AsyncOpenAI().chat.completions.parse(...)with a reused client under concurrency, using both a small Pydantic response and a roughly 49 KB structured response.DefaultAioHttpClient(), and the documented injected legacyhttpx.AsyncClient()escape hatch.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?
Please share the exact resolved versions, ideally the relevant lines from
pip freezeor your lockfile:openai,httpx2,httpcore2,httpx,aiohttp,anyio,jiter, andpydantic, plus your Linux distribution and whether you use uvloop.Please share a minimized version of client construction and the OpenAI call site. In particular: is one
AsyncOpenAIclient reused for the process lifetime, or is a new client created per request? How and when is it closed?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.Does v3.6.0 still reproduce when you explicitly inject a legacy HTTPX client?
If this is healthy while the default HTTPX2 and aiohttp configurations fail, that would strongly isolate the regression to the HTTPX2/common-client layer.
During the high-CPU window, could you capture a 30–60 second CPU profile or stack sample (
py-spy top,py-spy record, orpy-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.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?
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.
vmaurin commentedon Sep 2, 2026
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)
We observed the issue with both
and
We create a single client for the whole process that is reused during the full process timeline
We are using
client.chat.completions.createThen 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
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
(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)
Hughhhhcoder commentedon Sep 2, 2026
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.
aktaseren commentedon Sep 5, 2026
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.
test: add regression test for event loop blocking fix
vmaurin commentedon Sep 8, 2026
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
vmaurin commentedon Sep 9, 2026
I was wrong here, both the observed task are running with uvloop
NateRVance commentedon Sep 28, 2026
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:
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<3resolves this issue. My next debugging step will be to investigate an uvloop alternative.dajiaohuang commentedon Sep 29, 2026
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
AsyncOpenAIclient 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.NateRVance commentedon Sep 30, 2026
Thanks for the response! This is actually with multiple long-lived
AsyncOpenAIclients 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 AsyncOpenAIclient. We removed nest_asyncio, used the openaiAsyncOpenAIclient directly, and have rerun the experiment over the last day, obtaining the same behavior:This is all with python's
asyncio; we investigateduvloopbut 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.SantoshDhaladhuli commentedon Oct 6, 2026
I have faced a similar issue over time when transitioned to openai 3.x
dajiaohuang commentedon Oct 7, 2026
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 rampingpy-spy record -o profile.svg --pid <pid> --duration 30for a 30-second flame graphasyncio.all_tasks()is good, but also try callingtask.print_stack()on any tasks that look suspicious or are accumulatingThe 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.
jbeckwith-oai commentedon Oct 7, 2026
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: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:
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:
Thanks again for the patience here. The health-check report and the legacy HTTPX comparison were particularly helpful in narrowing this down.
vmaurin commentedon Oct 14, 2026
On our side, we indeed use this mix (container over Ubuntu 24.04)
We applied the proposed fix on our production system
and we can confirm the CPU is stable 👍
jbeckwith-oai commentedon Oct 19, 2026
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
5606fb4with SDK 3.6.0, truststore 0.10.4, and OpenSSL 3.0.22. Each run made 500 successfulmodels.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:
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!