Skip to content

Benchmarks

Raw numbers behind the throughput race on the home page, generated by perf.py against the bundled Rust/axum JSON server, 10,000 requests at concurrency 100 (plus a 1,000-request warm-up, untimed).

lothc, lothc-msgspec, and lothc-pydantic are the same HTTPClient, differing only in response_data_type: lothc decodes into a plain dict (unvalidated), lothc-msgspec into a real nested msgspec Struct, and lothc-pydantic into a real nested pydantic BaseModel — both "typed" rows decode the exact same nested user/data/pagination response shape the server returns, fully parsed and constructed, not a shortcut subset of it.

Every other library in this table gets exactly one row, because none of them have an equivalent to response_data_type at all — httpx/httpx2/aiohttp/niquests/aiosonic/pyreqwest's .json() (or similar) hands back a plain dict, full stop. Decoding that into a real, validated object of your own type is something you'd write and maintain yourself on top of any of them, because it isn't a feature those clients offer — it's a first-class, built-in part of lothc.

Library Total Time (s) Throughput (req/s) Relative (x) CPU (s) Peak Py Mem (MB) Min (ms) P50 (ms) P95 (ms) P99 (ms) Max (ms) Mean (ms)
httpx 9.17 1,090 1.00 9.17 2.33 2.78 66.68 253.07 393.30 838.99 91.34
httpx2 2.81 3,559 3.30 2.81 2.52 6.40 27.51 30.83 32.64 58.36 27.94
niquests 2.65 3,780 3.50 2.65 9.00 8.84 25.52 29.51 47.66 54.39 26.29
aiosonic 0.56 17,990 16.50 0.56 1.17 1.27 6.12 8.53 10.12 13.43 5.52
aiohttp 0.40 25,187 23.10 0.40 1.33 0.72 4.36 6.11 7.54 9.15 3.94
lothc-pydantic 0.20 48,955 44.90 0.20 0.63 1.20 2.02 2.14 2.86 3.10 2.02
lothc 0.20 50,347 46.20 0.20 0.63 0.51 1.95 2.51 2.97 3.37 1.96
lothc-msgspec 0.19 52,916 48.50 0.19 0.63 0.39 2.01 2.54 2.97 3.20 1.87
pyreqwest 0.18 54,556 50.00 0.18 0.51 0.78 1.91 2.43 2.61 3.04 1.81

Sorted slowest to fastest (x = throughput relative to the slowest library, httpx, in this run).

Worth calling out

lothc-msgspec outran even the unvalidated lothc row. Decoding into a real, nested, fully-constructed msgspec Struct was faster than the plain dict path in this run — msgspec's own JSON decoder is fast enough at parsing straight into structs that doing more work (real typed construction, not just a dict) cost nothing extra. lothc-pydantic landed close behind lothc too — full pydantic-core validation, at essentially the same speed as no validation at all.

Re-run this yourself from a checkout of the repo with task perf (see Development, and CLAUDE.md's Benchmarks section for the full set of options) — pass --libs "lothc lothc-msgspec lothc-pydantic" to isolate just the decode-target comparison.

Sync sequential

The race above is pooled-concurrency, async-only — it says nothing about a plain blocking client making one request at a time, which is its own common case (a script, a cron job, code that isn't async at all). This one covers exactly that: one non-concurrent request at a time against each library's blocking sync client, no event loop involved. aiohttp and aiosonic have no sync client so aren't included; requests — the classic blocking-only library, with no async client of its own — only appears here, never in the race above.

10,000 sequential requests (plus a 100-request warm-up, untimed) against the same bundled Rust/axum JSON server:

Library Total Time (s) Throughput (req/s) Relative (x) CPU (s) Peak Py Mem (MB) Min (ms) P50 (ms) P95 (ms) P99 (ms) Max (ms) Mean (ms)
requests 3.35 2,986 1.00 3.09 1.00 0.28 0.33 0.35 0.52 0.85 0.34
niquests 2.87 3,489 1.20 2.63 0.96 0.23 0.28 0.32 0.50 1.16 0.29
httpx 2.34 4,270 1.40 2.16 0.75 0.16 0.22 0.33 0.50 14.48 0.23
httpx2 2.05 4,889 1.60 1.86 0.39 0.17 0.20 0.22 0.29 0.76 0.20
lothc-pydantic 0.83 12,099 4.10 0.47 0.32 0.05 0.08 0.09 0.11 2.81 0.08
lothc 0.77 12,988 4.30 0.42 0.32 0.05 0.07 0.09 0.13 0.41 0.08
lothc-msgspec 0.74 13,484 4.50 0.39 0.31 0.04 0.07 0.08 0.09 0.23 0.07
pyreqwest 0.69 14,394 4.80 0.35 0.32 0.04 0.07 0.08 0.08 0.29 0.07

Sync-sequential HTTP client throughput race — pyreqwest and lothc finish first, requests and niquests take much longer

Without pooled concurrency to hide it, per-request Python overhead dominates far more starkly: pyreqwest/lothc/lothc-msgspec/lothc-pydantic are within a few percent of each other and roughly 4-5x faster than httpx/httpx2/niquests/requests, none of which have a Rust transport underneath them.

Re-run this yourself with task perf-single-call-sync (see Development) — regenerate the chart above from a fresh results file with task perf-svg -- results/single-call- sync-<run_id>.json --out assets/perf-race-sync.svg.