hawkapi-ratelimit
Production-grade rate limiting for HawkAPI. Token bucket + sliding window. In-memory + Redis-Lua backends. Per-IP / per-user / per-API-key / composite identities. Standard X-RateLimit-* + Retry-After headers. Decorator or middleware.
Install
pip install hawkapi-ratelimit
pip install 'hawkapi-ratelimit[redis]' # adds redis client for cluster-safe limits
Quickstart
from hawkapi import HawkAPI
from hawkapi_ratelimit import init_ratelimit, rate_limit
app = HawkAPI()
init_ratelimit(app) # default: MemoryLimiter (single-process)
@app.get("/search")
@rate_limit(rate=10, per=60) # 10 requests per minute, per IP
async def search(request, q: str):
return {"q": q}
When the budget is exhausted the route returns 429 Too Many Requests with Retry-After and the X-RateLimit-* headers set. Allowed requests get the same headers so clients can self-pace.
Strategies
from hawkapi_ratelimit import MemoryLimiter, init_ratelimit
# Token bucket — smoothed, allows short bursts (default).
init_ratelimit(app, limiter=MemoryLimiter(strategy="token_bucket"))
# Sliding window — precise, rejects the moment the threshold is crossed.
init_ratelimit(app, limiter=MemoryLimiter(strategy="sliding_window"))
Redis backend
For multi-process / multi-host deployments, swap in RedisLimiter — atomic check-and-increment via Lua scripts, cluster-safe:
from hawkapi_ratelimit import RedisLimiter, init_ratelimit
init_ratelimit(
app,
limiter=RedisLimiter(
url="redis://localhost:6379/0",
strategy="token_bucket",
key_prefix="myapp:rl:",
socket_timeout=0.2, # < 1s — don't let the limiter add tail latency
fail_closed=False, # default: allow on Redis error (fail-open)
),
)
Both strategies are implemented as single atomic Lua scripts — no read-then-write race.
Identity strategies
from hawkapi_ratelimit import api_key, composite_key, header_key, ip_key, rate_limit, user_key
# Per-IP — default, honors X-Forwarded-For only if you opt in.
@rate_limit(rate=100, per=60, identity=ip_key(trusted_proxy=True))
async def f(request): ...
# Per-authenticated-user (reads ``request.scope["user"]``).
@rate_limit(rate=1000, per=60, identity=user_key(attribute="user_id"))
async def f(request): ...
# Per-API-key (reads Authorization: Bearer <token>).
@rate_limit(rate=10000, per=60, identity=api_key())
async def f(request): ...
# Combine — IP AND user, separately budgeted.
@rate_limit(rate=100, per=60, identity=composite_key(ip_key(), user_key()))
async def f(request): ...
ip_key(trusted_proxy=False) (the default) reads request.client.host — the socket peer. ip_key(trusted_proxy=True) reads the left-most token of X-Forwarded-For (RFC-correct client). Never enable trusted_proxy=True unless your edge proxy strips inbound X-Forwarded-For from clients.
Middleware mode
For a global policy across the whole app:
from hawkapi_ratelimit import RateLimit, RateLimitMiddleware
app.add_middleware(
RateLimitMiddleware,
limit=RateLimit(rate=1000, per=60),
exclude_paths=("/health", "/metrics"),
)
Per-route @rate_limit decorators stack on top of the middleware — they share the same backend.
Response shape
On rejection:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000060
Retry-After: 7
{"detail": "rate limit exceeded", "retry_after": 7.0}
Known accepted risks
These are real tradeoffs that operators should layer additional protection around.
user_key()collapses anonymous traffic to one shared bucket ("user:anon"). A single attacker can exhaust the anonymous budget. Pair withcomposite_key(ip_key(), user_key())or restrict the route to authenticated users.- Middleware does not set
X-RateLimit-*on allowed responses — only on 429. The decorator sets them on every response. If you need response-headers everywhere, use the decorator (or attach headers in a downstream middleware). @rate_limitskipped when handler has noRequestparameter — silently no-ops because there's nothing to key on. AUserWarningfires at import time if you forget it.MemoryLimiteris single-process — multi-worker deployments must useRedisLimiterfor cluster-wide budgets.
Security notes
trusted_proxy=Falseby default — opt in only when your edge strips client-suppliedX-Forwarded-For. HonoringXFFfrom an untrusted source lets attackers forge their identity by setting the header.- Key length capped at 256 chars — prevents unbounded memory growth in
MemoryLimiterwhen an attacker submits very long identifiers. MemoryLimiterevicts the oldest entries when the store exceedsmax_keys(default 100k). The store cannot grow without bound.- Redis
socket_timeout=0.2s— short, so a partially-degraded Redis cannot add seconds of tail latency to every request. - Fail-open by default (
RedisLimiter(fail_closed=False)) — Redis outage = log warning and allow the request, rather than DoS'ing the app. Choosefail_closed=Truefor endpoints where rate-limit MUST hold (admin / payments). - Lua atomicity — token-bucket refill + consume runs in a single
EVALround, so concurrent workers cannot race the budget.
Development
git clone https://github.com/Hawk-API/hawkapi-ratelimit.git
cd hawkapi-ratelimit
uv sync --extra dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run pyright src/
License
MIT.
Metadata
Release files for hawkapi-ratelimit 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hawkapi_ratelimit-0.1.2.tar.gz | 27.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hawkapi_ratelimit-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.4 kB
Release files / hawkapi_ratelimit-0.1.2.tar.gz
| Download URL | hawkapi_ratelimit-0.1.2.tar.gz |
|---|---|
| Size | 27.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
16c8e8de4700480d89c58ccf0328077e4cfcfd23536e4f1605ee864a470adb79
|
|
BLAKE2b-256 checksum How to use checksums |
aa819e27957691380b12e73b70aacd7b195d86e541c91e12d0da875a2cb289b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency logRelease files / hawkapi_ratelimit-0.1.2-py3-none-any.whl
| Download URL | hawkapi_ratelimit-0.1.2-py3-none-any.whl |
|---|---|
| Size | 16.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a59f8a22ac00b667e0f2389d70de7770c5a243423c37a4cb9ed6b3e60fd50e2e
|
|
BLAKE2b-256 checksum How to use checksums |
020361f24b94c45cdf264da1e8235ce023aac33baab7629b9fc2f510abebf0b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency log