Skip to main content

Rate limiting for HawkAPI — token bucket + sliding window, Redis + in-memory backends

Project description

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 with composite_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_limit skipped when handler has no Request parameter — silently no-ops because there's nothing to key on. A UserWarning fires at import time if you forget it.
  • MemoryLimiter is single-process — multi-worker deployments must use RedisLimiter for cluster-wide budgets.

Security notes

  • trusted_proxy=False by default — opt in only when your edge strips client-supplied X-Forwarded-For. Honoring XFF from an untrusted source lets attackers forge their identity by setting the header.
  • Key length capped at 256 chars — prevents unbounded memory growth in MemoryLimiter when an attacker submits very long identifiers.
  • MemoryLimiter evicts the oldest entries when the store exceeds max_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. Choose fail_closed=True for endpoints where rate-limit MUST hold (admin / payments).
  • Lua atomicity — token-bucket refill + consume runs in a single EVAL round, 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hawkapi_ratelimit-0.1.1.tar.gz (27.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hawkapi_ratelimit-0.1.1-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

Details for the file hawkapi_ratelimit-0.1.1.tar.gz.

File metadata

  • Download URL: hawkapi_ratelimit-0.1.1.tar.gz
  • Upload date:
  • Size: 27.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hawkapi_ratelimit-0.1.1.tar.gz
Algorithm Hash digest
SHA256 733c65f7b9be3449a7ac7ebb2559dd74caef1bc03762d355be2c2d4da8382bd1
MD5 479267cb65a8f6b9b48fb62a4337324e
BLAKE2b-256 415dec1e6e71c81fa8acbdfd077ab8e973e71b24148a92188d5bdc40db807ce0

See more details on using hashes here.

Provenance

The following attestation bundles were made for hawkapi_ratelimit-0.1.1.tar.gz:

Publisher: release.yml on Hawk-API/hawkapi-ratelimit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hawkapi_ratelimit-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for hawkapi_ratelimit-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3238ef29f8183bcf5d6e50e05ea3d3587b6538369102a70ee8ac92d74a667135
MD5 86cf31da22a74cf17bedebe36ebc586c
BLAKE2b-256 fdc346e0e07b38debd41859aab7c02d1cd749f90316ba8f5b182f251ad992833

See more details on using hashes here.

Provenance

The following attestation bundles were made for hawkapi_ratelimit-0.1.1-py3-none-any.whl:

Publisher: release.yml on Hawk-API/hawkapi-ratelimit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page