Compared with others¶
headgate is a Swiss Army knife for HTTP API clients. Most libraries do one job well: send requests, retry, rate-limit, cache, break circuits, mock. headgate does all of those jobs in one base class, and makes them agree with each other. This page shows what that buys you, library by library, and when another tool is the better pick.
built in with another library, or code you write not available
HTTP clients¶
requests, niquests, httpx and aiohttp send HTTP requests, and do it well. headgate runs on httpx2 (pydantic's fork of httpx) and adds everything a client for someone else's API needs on top. It can also send through curl_cffi when you need libcurl.
| requests | niquests | httpx | aiohttp | headgate | |
|---|---|---|---|---|---|
| Async | |||||
| HTTP/2 | |||||
| Retries with backoff | |||||
Waits for Retry-After |
|||||
| One deadline over every retry | |||||
| Rate limiting that obeys the server | |||||
| Circuit breaker | |||||
| Caching | |||||
| Pagination | |||||
| Server-sent events | |||||
| RFC 9457 problem details parsed | |||||
| Every credential kept off other hosts | |||||
| SSRF guard with DNS pinning | |||||
| Response size cap | |||||
Ignores proxy variables and .netrc by default |
|||||
| Secrets redacted from error messages | |||||
| Test fakes | |||||
| Sync API |
When a redirect leaves the origin, requests and httpx drop the Authorization header, and aiohttp drops Cookie and
Proxy-Authorization too. headgate drops those and every other header and query parameter its auth declares, such as
an X-API-Key, on every hop, next-page links included: see security.
The stack headgate replaces¶
| Job | What people usually add | In headgate |
|---|---|---|
| Retries | tenacity, stamina, backoff, httpx-retries, aiohttp-retry | RetryPolicy |
| Rate limits | aiolimiter, pyrate-limiter, limits | RateBudget |
| Caching | hishel, requests-cache, aiohttp-client-cache, CacheControl | HttpCache, on hishel |
| Circuit breaking | pybreaker, aiobreaker, purgatory, circuitbreaker | BreakerPolicy |
| Server-sent events | httpx-sse, aiohttp-sse-client | stream().aiter_events() |
| Pagination | a loop you write per endpoint | paginate() |
| Test doubles | respx, pytest-httpx, aioresponses, responses | headgate.testing; vcrpy cassettes work too |
| Telemetry | OpenTelemetry instrumentation packages | typed events and an OpenTelemetry observer |
Fewer dependencies is the small win. The big one is that the layers know about each other, which separate libraries cannot:
- A cache hit is never retried, and spends no rate turn, because the cache sits below the retry layer and above the rate budget.
- The retry layer and the rate budget read
Retry-Afterthe same way, so the retry's wait and the budget's pause end together instead of stacking. - One deadline bounds everything: every attempt, every backoff, every rate-budget wait and the body read.
- A breaker counts only the host's own failures: a 404 your code asked for, or a call you cancelled, does not open it.
Library by library¶
| tenacity | stamina | backoff | urllib3 Retry |
headgate | |
|---|---|---|---|---|---|
| Retries any code, not just HTTP | |||||
| Async | |||||
| Knows which HTTP requests are safe to re-send | |||||
Waits for Retry-After |
|||||
| Total time limit | |||||
| Retry budget shared across calls | |||||
| Idempotency keys for safe POST retries | |||||
| Hedged requests | |||||
| Instant retries in tests |
Pick stamina or tenacity to retry something that is not an HTTP call, such as a database query. stamina's
on= hook (25.2 and later) and tenacity's wait_exception can honor Retry-After with a little code. backoff's
repository is archived, and it has not been released since 2022.
| aiolimiter | pyrate-limiter | headgate | |
|---|---|---|---|
| Async | |||
| Several rates at once | |||
| Shared across processes (Redis, SQLite) | |||
| Pauses when the server says 429 or 503 | |||
Reads RateLimit and X-RateLimit-* headers |
|||
| Slows down on pushback (adaptive) | |||
| Separate lanes per path | |||
| Cap on requests in flight |
Pick pyrate-limiter when many processes or machines must share one quota.
| hishel | requests-cache | aiohttp-client-cache | headgate | |
|---|---|---|---|---|
| Async | ||||
Follows Cache-Control (RFC 9111) |
||||
| Your own rules for APIs that send no cache headers | ||||
| Entries kept apart per credential | ||||
| Stores only answers your code accepted |
headgate's cache is hishel, configured with your rules and wrapped so it is safe to share between
credentials. Pick hishel directly when you want the provider's Cache-Control to decide; it supports httpx2
too.
| pybreaker | aiobreaker | purgatory | headgate | |
|---|---|---|---|---|
| Async | ||||
| One breaker per host, automatically | ||||
| Counts only the host's own failures | ||||
| Retries stop while it is open | ||||
| State shared across processes (Redis) |
Pick pybreaker or purgatory to guard something that is not HTTP, or to share breaker state across machines.
| respx | pytest-httpx | aioresponses | vcrpy | headgate.testing | |
|---|---|---|---|---|---|
| Canned responses | |||||
| Record and replay real traffic | |||||
| Failure schedules (fail twice, then succeed) | |||||
| A fake clock, so retries and rate waits are instant | |||||
| Breaker state isolated per test |
respx and pytest-httpx patch httpx, not httpx2, so they do not see a headgate client's requests.
headgate.testing is built for it, and vcrpy 8.3 and later record httpx2; headgate ships
filters that keep credentials out of the cassettes.
| What it is | How headgate differs | |
|---|---|---|
| uplink | declare endpoints with decorators on an interface | headgate endpoints are ordinary methods, and resilience is built in rather than added per decorator |
| openapi-python-client | generates a typed client from an OpenAPI document | use it when you have a spec; headgate is for APIs without one, or where you want control |
| Stainless SDKs (openai, anthropic) | generated SDKs with retries and timeouts built in | headgate brings that quality to a client you write yourself, and understands the same retry-after-ms and x-should-retry headers |
When to use something else¶
- Retrying code that is not an HTTP call (a database query, a file operation): use stamina, or tenacity when you need its full flexibility.
- Python before 3.14: headgate needs 3.14 for its template-string paths. Use httpx with stamina, and add rate limiting and caching yourself.
- Upstream httpx: headgate runs on httpx2 and refuses httpx objects, to catch the mix-up early. If your code has to stay on httpx, headgate is not for you yet.
- A synchronous codebase: headgate is async only. requests or httpx's sync client is the simpler choice.
- One or two calls in a script: a plain httpx2 call is fine. headgate pays off when a client has several endpoints, several consumers, or has to survive a provider's bad day.
- A quota shared across machines: headgate's rate budget and breakers live in one process. Use pyrate-limiter or purgatory with Redis for that.
Why httpx2¶
headgate is built and tested on httpx2, pydantic's fork of httpx, and on nothing
else. Its API is httpx's, so moving a client over is mostly an import change: import httpx2 where you had
import httpx, and the same for the exceptions you catch. headgate refuses an httpx object anywhere it takes an httpx2
one, with a TypeError naming the mistake, rather than failing later in a confusing way.