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.2.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.2-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: hawkapi_ratelimit-0.1.2.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.2.tar.gz
Algorithm Hash digest
SHA256 16c8e8de4700480d89c58ccf0328077e4cfcfd23536e4f1605ee864a470adb79
MD5 3f7f5bd6d69bd77823b6a6dcb6420468
BLAKE2b-256 aa819e27957691380b12e73b70aacd7b195d86e541c91e12d0da875a2cb289b4

See more details on using hashes here.

Provenance

The following attestation bundles were made for hawkapi_ratelimit-0.1.2.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.2-py3-none-any.whl.

File metadata

File hashes

Hashes for hawkapi_ratelimit-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a59f8a22ac00b667e0f2389d70de7770c5a243423c37a4cb9ed6b3e60fd50e2e
MD5 a77d00281e320fe9e43066eb97b0c7fe
BLAKE2b-256 020361f24b94c45cdf264da1e8235ce023aac33baab7629b9fc2f510abebf0b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for hawkapi_ratelimit-0.1.2-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