D1: the async residual is CPython, not the SDK — evidence, uvloop extra, PERFORMANCE.md - #32
Merged
Merged
Conversation
…/J5)
Run 5 recorded check_access at p50 40.2 ms / 311 rps against Go, Java and
Rust's ~10 ms / ~850 rps at the same 16-way concurrency, and the plan's
reading was that the async rewrite had left something slow in this SDK. It
had not. The measurement that settles it is running the same client against
a server that does no work at all.
Against an out-of-process stub answering a fixed JSON body, 16 in-flight:
raw httpx, no cookie jar p50 44.0 ms 347 rps 1 589 us CPU/call
raw httpx + cookie jar p50 47.2 ms 324 rps 2 121 us CPU/call
AsyncAxiamClient.check_access p50 47.3 ms 324 rps 2 172 us CPU/call
The SDK costs ~50 us/call over raw httpx with the same jar — about 2% of
client CPU — and a cProfile of the hot path puts every one of the top 28 cost
centres in httpcore/anyio (connection_pool._assign_requests_to_connections
alone calls connection.is_idle 611 times per request; six module-import
lookups happen per call inside anyio itself). No axiam_sdk frame appears.
The ~310 rps is a per-process CPython ceiling, not an AXIAM number: three
client processes against that same zero-work stub reached 320+308+301 =
929 rps aggregate, each capped at ~310 — which is run 5's 311 to within
noise. And the plan's "p50 <= 20 ms" target is arithmetically unreachable in
one process: 16 in-flight calls x ~2 ms CPU each on a single event-loop
thread has a floor near 32 ms regardless of how fast the server answers.
What changed here, all evidence-backed:
* a [speed] extra installing uvloop — measured at 2 182 -> 1 735 us CPU per
call (-20%), p95 68 -> 55 ms, throughput +7%. The SDK still does not
install a loop policy; that belongs to the application.
* PERFORMANCE.md, with the numbers, the method, the reproduction, and the
two things that actually help (uvloop; scale with processes, not with
in-flight calls per process).
* README points at both.
Also recorded there: the four suspects the plan listed, and which the
evidence indicts. Per-request session churn is absent (one pooled client,
held for the client's lifetime). Sync-in-async and pydantic construction sit
inside the ~50 us the SDK adds and never reach the profile. The cookie jar is
the one real SDK-adjacent cost at ~530 us/call, and it is not removable — the
AXIAM session IS cookies (CONTRACT.md §3), and reimplementing cookie policy
to save CPU is not a trade to make in an auth client.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ubrFbqsMkBqC5gwadPsDu
`ruff format --check .` covers Python blocks inside Markdown, and the uvloop example used one blank line where the formatter wants two around a top-level definition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ubrFbqsMkBqC5gwadPsDu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes D1/J5 of
claude_dev/improvement-after-run5-benchmark.md(ilpanich/axiam).The finding
Benchmark run 5 put this SDK's
check_accessat p50 40.2 ms / p95 116.8 ms / 311 rps against Go, Java and Rust's ~10 ms / ~60 ms / 835–869 rps at the same 16-way concurrency. The plan's reading was that the async rewrite had left something slow inaxiam_sdk. It had not.The measurement that settles it is running the same client against a server that does no work at all — an out-of-process stub (a threaded stub in the client's own process competes for the GIL and is indistinguishable from client cost), 16 in-flight:
httpx.AsyncClient, no cookie jarhttpx.AsyncClient+ cookie jarAsyncAxiamClient.check_access1. The SDK is not the residual. ~50 µs/call over raw
httpxwith the same jar — about 2% of client CPU. AcProfileof the hot path puts every one of the top 28 cost centres inhttpcore/anyio:connection_pool._assign_requests_to_connectionsalone callsconnection.is_idle611 times per request, and six module-import lookups happen per call insideanyio/httpcorethemselves. Noaxiam_sdkframe appears.2. ~310 rps is a per-process CPython ceiling, not an AXIAM number. The stub is not the bottleneck: three client processes driving it concurrently reached 320 + 308 + 301 = 929 rps aggregate, each capped at the same ~310. Run 5's 311 rps against the real server is that same ceiling.
3. The plan's
p50 ≤ 20 mstarget is arithmetically unreachable in one process. 16 in-flight × ~2 ms CPU each on a single event-loop thread has a floor near 32 ms regardless of how fast the server answers. The 44–47 ms above is almost entirely client-side queueing against a zero-latency server.What changed
[speed]extra installinguvloop. Measured on the same harness: client CPU 2 182 → 1 735 µs/call (−20%), p95 68 → 55 ms, throughput +7%. The SDK still does not install a loop policy — choosing the event loop belongs to the application, not to a library it happens to import.PERFORMANCE.md— the numbers, the method, the reproduction, and the two things that actually help: use uvloop, and scale with processes rather than with in-flight calls per process.No source changes; the SDK's behaviour is unchanged.
The four suspects, and what the evidence indicts
httpx.AsyncClient, built lazily and held for the client's lifetime.AccessResultwould trade the typed contract for well under 1% of the request.check()— see (2). Not indicted.The cookie jar is the one non-trivial SDK-adjacent cost (~530 µs/call, 25% of client CPU):
http.cookiejaris pure Python and httpx re-runs policy and domain matching per request and response. It is not removable — the AXIAM session is cookies (CONTRACT.md §3) — and reimplementing cookie policy to save CPU is not a trade to make in an auth client.Related
event_loop/client_worker_threads, so a CPU figure always says which loop produced it).Generated by Claude Code