ratelimit-headers
Zero-dependency HTTP rate-limit header parser — pure Python stdlib only.
Parses heterogeneous rate-limit header formats from GitHub, Stripe, Twitter/X, the IETF draft spec, and RFC 7231 into a single canonical RateLimitState dataclass.
# From PyPI (once published)
pip install ratelimit-headers
# From local clone
pip install .
from ratelimit_headers import parse
state = parse(response_headers={
"X-RateLimit-Limit": "5000",
"X-RateLimit-Remaining": "4999",
"X-RateLimit-Reset": "1893456000",
})
print(state.limit) # 5000
print(state.remaining) # 4999
print(state.reset) # datetime(2099, 12, 31, tzinfo=UTC)
print(state.source) # "github"
⚡ Performance & Benchmarks
No competitors exist — this is the first pure-stdlib rate-limit header normaliser on PyPI.
python3 benchmarks/run_benchmark.py
Why ratelimit-headers?
Every major HTTP API uses a different rate-limit header convention. Existing PyPI packages (aiolimiter, async-rate-limit) are enforcement libraries, not parsing/normalisation utilities. No pure-stdlib package normalised these heterogeneous headers into a uniform struct.
ratelimit-headers is a single ~150 LOC library + CLI that handles:
| Provider | Header namespace | Reset format |
|---|---|---|
| GitHub | X-RateLimit-* |
Unix epoch |
| Stripe | X-RateLimit-* |
Unix epoch |
| Twitter/X | x-rate-limit-* |
Unix epoch |
| IETF draft | RateLimit-* + Retry-After |
delta-seconds / HTTP-date |
| RFC 7231 | Retry-After |
delta-seconds / HTTP-date |
Key Features
- Pure stdlib — zero third-party dependencies
- Five parser formats — GitHub, Stripe, Twitter/X, IETF draft, RFC 7231
- Canonical dataclass —
RateLimitStatewithlimit,remaining,reset,retry_after,window,source - CLI —
parse,detect,waitsubcommands - Never crashes — malformed/garbage input returns
RateLimitState(source="unknown") - Type-safe —
isinstanceguards preventAttributeErroron non-str keys - Fuzz-tested — NaN, hex, oversized ints, non-UTF8 bytes all handled safely
API Reference
RateLimitState dataclass
from ratelimit_headers import RateLimitState
@dataclass(frozen=True, slots=True)
class RateLimitState:
limit: Optional[int] # requests allowed
remaining: Optional[int] # requests remaining
reset: Optional[datetime] # UTC reset time
retry_after: Optional[int] # seconds until retry
window: Optional[int] # window size in seconds
source: Optional[str] # format name
parse(request_headers=None, response_headers=None) -> RateLimitState
Parses rate-limit headers from an HTTP response. Returns a RateLimitState. Never raises — malformed input yields RateLimitState(source="unknown").
from ratelimit_headers import parse
state = parse(response_headers=headers)
detect_format(headers: dict) -> str
Returns the detected format name: "github" | "stripe" | "twitter" | "ietf" | "retry-after" | "unknown".
format_name(source: str) -> str
Human-readable name for a format identifier.
CLI
# Parse headers from command-line arguments
ratelimit-headers parse "X-RateLimit-Limit: 100" "X-RateLimit-Remaining: 99"
ratelimit-headers parse --json "X-RateLimit-Limit: 100"
# Detect format from a file
ratelimit-headers detect --input headers.txt
ratelimit-headers detect --json < headers.txt
# Wait until rate-limit resets (reads from stdin)
ratelimit-headers wait --max 300 < headers.txt
CLI JSON output schema
{
"limit": 5000,
"remaining": 4999,
"reset": "2099-12-31T23:59:59+00:00",
"retry_after": null,
"window": 3600,
"source": "github"
}
Limitations
- No enforcement — this library parses headers only; it does not throttle, back off, or retry
- No async — synchronous only; no
asynciooraiohttpintegration - No server-side component — not a middleware or reverse-proxy plugin
- No retry orchestration — does not retry failed requests
- Does not parse quoted-parameter syntax inside header values (e.g.
Limit: 100; w=3600)
Known issues (fuzzing-discovered, non-blocking for v0.1.0)
The v0.1.0 release ships with two known minor issues discovered during cycle_51 adversarial fuzzing (benchmarks/adversarial/FUZZING_REPORT.md). Both are classified Medium non-blockers with trivial 1-LOC fixes queued for cycle_52:
- F-10 —
reset=0epoch yieldsremaining=100instead of 0. When the upstream server sendsX-RateLimit-Reset: 0(Unix epoch, already-elapsed window),parse()returnsremaining=100rather than0because the window-elapsed branch setsremaining = limitinstead of 0. Trivial 1-LOC fix. - F-15 —
parsers.retry_afterpropagatesRuntimeErrorinstead of returningRateLimitState(source="unknown"). All other parsers are wrapped in defensiveexcept Exception, butparsers.retry_afterlets internalRuntimeErrors escape. Trivial 1-LOCtry/exceptwrap. - F-28 —
coerce_intraisesOverflowErroron values >2^63. Treated as Info (not Medium) because the function is not on the public surface — it is reached only when callers bypass the documentedparse()entry point withrequest_headers=for an unsupported scheme. - F-NEW-1 —
parse_http_date(None)raisesAttributeError. Same surface as F-28; non-public helper, defensiveexcept Exceptionqueued.
Non-Goals
- NOT a rate limiter / throttler / enforcement library
- NOT an HTTP client
- NOT async
- NOT a policy enforcer
- NOT a server-side component
Test suite
pytest --collect-only -q | tail -1
155 tests across 9 test files — all passing.
Full adversarial fuzzing report:
benchmarks/adversarial/FUZZING_REPORT.md.
License
MIT
Metadata
Release files for ratelimit-headers 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ratelimit_headers-0.1.0.tar.gz | 364.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ratelimit_headers-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 377.8 kB
Release files / ratelimit_headers-0.1.0.tar.gz
| Download URL | ratelimit_headers-0.1.0.tar.gz |
|---|---|
| Size | 364.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
addb344eb31411fec112a805794e0621da05ac1bc23cd66577a4616f82664ebc
|
|
BLAKE2b-256 checksum How to use checksums |
ee471d2b3321a781940fd6f9fb03a78f0cc638b63a3e3a4b2cb2cbe592cb16ea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|
Release files / ratelimit_headers-0.1.0-py3-none-any.whl
| Download URL | ratelimit_headers-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1d375a1cda3d382a04f001c621e89dec276359102def142c2e27244dca28d93f
|
|
BLAKE2b-256 checksum How to use checksums |
d59cbb3dd4a21fd9a525f551613d56379a4e3e21e38c48a6895c14907c6a7a0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|