Rate Limiting
The ratelimit module provides shared infrastructure for rate limiting across Django and FastAPI. This includes Lua scripts for atomic Redis operations, data models for rate limit configuration, and key generation utilities.
Lua Script
PROJECT_RATE_LIMITER_LUA handles atomic rate limit checking with optional override support. It checks whether current + increment_amount would exceed the limit before incrementing (using INCRBY for element-based counting), checks for per-project overrides stored in Redis hashes, falls back to organization-level overrides, and returns {is_over_limit, request_count, ttl, effective_max_requests, effective_expiry} in a single atomic operation. The effective values reflect any applied overrides, which is important for accurate IETF w= (window) parameters. The priority chain is: project override → org override → defaults. When no override keys are provided (empty strings), it behaves as a simple rate limiter.
Key Format
Rate limit keys follow consistent patterns:
- Counter:
ratelimit:{endpoint}:{organization_id}:{project_id} - Project override:
ratelimit_override:{endpoint}:{organization_id}:{project_id} - Org override:
ratelimit_org_override:{endpoint}:{organization_id}
The endpoint portion matches your framework's route pattern (e.g., /datasets/{dataset_id}/search for FastAPI).
Core
application_kit.ratelimit
Shared rate limiting components for Django and FastAPI.
This module contains the common Lua scripts, data models, and key generation functions used by both the Django and FastAPI rate limiting implementations.
RateLimitOverride
Bases: BaseModel
Rate limit override configuration stored in Redis.
RateLimitResult
dataclass
RateLimitResult(
is_over_limit,
request_count,
ttl,
max_requests,
policy_name="requests",
window=0,
)
Result of a rate limit check.
Supports IETF draft draft-ietf-httpapi-ratelimit-headers-10 via
named policies. Each result carries a policy_name (e.g. "requests",
"elements") and an optional window (full window size in seconds).
Multiple results can be combined into comma-separated RateLimit-Policy
and RateLimit structured headers.
to_policy_item
to_policy_item()
Format this result as an IETF RateLimit-Policy list member.
Returns e.g. '"requests";q=100;w=60' when window > 0,
or '"requests";q=100' when window is 0 (omitted).
Source code in application_kit/ratelimit.py
178 179 180 181 182 183 184 185 186 187 | |
to_status_item
to_status_item()
Format this result as an IETF RateLimit list member.
Returns e.g. '"requests";r=82;t=45'.
Source code in application_kit/ratelimit.py
189 190 191 192 193 194 | |
to_headers
to_headers()
Generate IETF rate limit headers for a single policy.
Source code in application_kit/ratelimit.py
196 197 198 199 200 201 | |
from_lua_result
classmethod
from_lua_result(raw, *, policy_name='requests')
Create a RateLimitResult from the Lua script output.
| PARAMETER | DESCRIPTION |
|---|---|
raw
|
List returned by PROJECT_RATE_LIMITER_LUA: [is_over_limit, request_count, ttl, max_requests, expiry]
TYPE:
|
policy_name
|
IETF policy name for structured headers.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
RateLimitResult
|
RateLimitResult instance |
Source code in application_kit/ratelimit.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | |
normalize_path
normalize_path(path)
Normalize a path for consistent rate limit key generation.
- Ensures the path starts with a leading slash
- Strips trailing slashes (except for root "/")
This ensures that "/api/users", "api/users", and "/api/users/" all produce the same rate limit key.
| PARAMETER | DESCRIPTION |
|---|---|
path
|
The path to normalize (e.g., route pattern or endpoint name)
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
str
|
Normalized path string |
Source code in application_kit/ratelimit.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
derive_policy_name
derive_policy_name(key_suffix)
Derive IETF policy name from the Redis key suffix.
Suffixes ending with "elements" are treated as element-based policies, everything else as request-based:
"" → "requests" "/traffic" → "traffic_requests" "/elements" → "elements" "/traffic/elements" → "traffic_elements"
Source code in application_kit/ratelimit.py
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
make_path_rate_limit_key
make_path_rate_limit_key(path)
Generate the Redis key for path-only rate limit counter (no project isolation).
Key format: ratelimit:{path}
| PARAMETER | DESCRIPTION |
|---|---|
path
|
The request path (should be normalized with
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
str
|
Redis key string for the rate limit counter. |
Source code in application_kit/ratelimit.py
267 268 269 270 271 272 273 274 275 276 277 278 | |
make_rate_limit_key
make_rate_limit_key(
endpoint_name, organization_id, project_id
)
Generate the Redis key for rate limit counter.
Source code in application_kit/ratelimit.py
281 282 283 284 285 286 287 | |
make_override_key
make_override_key(
endpoint_name, organization_id, project_id
)
Generate the Redis key for rate limit override lookup.
The endpoint_name is normalized (leading slash, no trailing slash) to ensure consistent keys regardless of how the caller formats the path.
Source code in application_kit/ratelimit.py
290 291 292 293 294 295 296 297 298 299 300 | |
parse_override_key
parse_override_key(key)
Parse an override key to extract endpoint, organization_id, and project_id.
| PARAMETER | DESCRIPTION |
|---|---|
key
|
Redis key in format "ratelimit_override:{endpoint}:{org_id}:{project_id}"
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple[str, int, int] | None
|
Tuple of (endpoint_name, organization_id, project_id) or None if parsing fails. |
Note
This handles endpoint names that contain colons by parsing from both ends: - The prefix is known and stripped from the left - org_id and project_id are parsed from the right (they're always integers) - Everything in between is the endpoint name
Source code in application_kit/ratelimit.py
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | |
make_org_override_key
make_org_override_key(endpoint_name, organization_id)
Generate the Redis key for organization-level rate limit override lookup.
Key format: ratelimit_org_override:{endpoint_name}:{organization_id}
| PARAMETER | DESCRIPTION |
|---|---|
endpoint_name
|
The endpoint name or route pattern.
TYPE:
|
organization_id
|
The organization ID, or None to skip org override lookup.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
str
|
Redis key string for the org override hash, or empty string if organization_id is None. |
Source code in application_kit/ratelimit.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 | |
parse_org_override_key
parse_org_override_key(key)
Parse an org override key to extract endpoint and organization_id.
| PARAMETER | DESCRIPTION |
|---|---|
key
|
Redis key in format "ratelimit_org_override:{endpoint}:{org_id}"
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple[str, int] | None
|
Tuple of (endpoint_name, organization_id) or None if parsing fails. |
Note
This handles endpoint names that contain colons by parsing from both ends: - The prefix is known and stripped from the left - org_id is parsed from the right (it's always an integer) - Everything in between is the endpoint name
Source code in application_kit/ratelimit.py
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | |
get_rate_limit_mode
get_rate_limit_mode()
Get rate limit mode from Bender configuration.
| RETURNS | DESCRIPTION |
|---|---|
RateLimitMode
|
RateLimitMode.on (default) - normal rate limiting, block when over limit |
RateLimitMode
|
RateLimitMode.off - completely disabled, skip all rate limit checks |
RateLimitMode
|
RateLimitMode.monitor - track violations but don't block, tag to Datadog |
Source code in application_kit/ratelimit.py
406 407 408 409 410 411 412 413 414 415 416 417 | |
is_rate_limit_disabled
is_rate_limit_disabled()
Check if rate limiting should be skipped for rate limiters that don't support monitor mode.
Returns True when RATE_LIMIT_MODE is "off" or "monitor". Rate limiters that
don't implement monitor mode (e.g., PathRateLimiter, custom RateLimiter
subclasses) should treat monitor mode as disabled — silently skipping rather
than incorrectly blocking requests.
Only ProjectRateLimiter and apply_rate_limit support monitor mode (they
check get_rate_limit_mode() directly and tag Datadog spans instead of blocking).
Example
class IpRateLimiter(RateLimiter):
def make_key(self, request: Request) -> str | None:
if is_rate_limit_disabled():
return None
ip = request.headers.get("X-Forwarded-For", "unknown")
return make_path_rate_limit_key(f"{request.url.path}:{ip}")
Source code in application_kit/ratelimit.py
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 | |
parse_last_policy_params
parse_last_policy_params(header_value)
Parse parameters from the last item in a structured rate limit header.
Given '"requests";q=100;w=60, "elements";q=5000;w=60', returns
{"q": "5000", "w": "60"} (from the last item).
This matches legacy behavior where the last limiter's headers would overwrite previous values.
Source code in application_kit/ratelimit.py
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 | |
FastAPI
application_kit.fastapi.ratelimit
RateLimitOverride
Bases: BaseModel
Rate limit override configuration stored in Redis.
RateLimiter
dataclass
RateLimiter(max_requests, expiry=1, policy_name='requests')
Base rate limiter class for FastAPI.
Subclass this and implement make_key() to create custom rate limiters.
Return None from make_key() to skip rate limiting for that request.
| ATTRIBUTE | DESCRIPTION |
|---|---|
max_requests |
Maximum requests allowed within the time window (must be >= 1).
TYPE:
|
expiry |
Time window in seconds (default: 1 second, must be >= 1).
TYPE:
|
Note
The base __call__ does not support "monitor" mode. When the rate limit
mode is set to "monitor", is_rate_limit_disabled() returns True and
make_key() should return None, effectively disabling rate limiting.
To support monitor mode (count but don't block), override __call__ and
check get_rate_limit_mode() directly. See ProjectRateLimiter for an example.
Example
class IpRateLimiter(RateLimiter):
def make_key(self, request: Request) -> str | None:
if is_rate_limit_disabled():
return None
ip = extract_user_ip_from_headers(request.headers)
if ip is None:
return None
return make_path_rate_limit_key(f"{request.url.path}:{ip}")
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If max_requests or expiry is less than 1. |
make_key
abstractmethod
make_key(request)
Build the Redis key for rate limiting.
| PARAMETER | DESCRIPTION |
|---|---|
request
|
The incoming Starlette/FastAPI request.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
str | None
|
Redis key string for the rate limit counter, or |
Source code in application_kit/fastapi/ratelimit.py
146 147 148 149 150 151 152 153 154 155 | |
PathRateLimiter
dataclass
PathRateLimiter(
max_requests, expiry=1, policy_name="requests"
)
Bases: RateLimiter
Rate limit by actual request path (no project isolation).
Each unique path gets its own rate limit counter. For example,
/users/123 and /users/456 have separate counters.
Useful for public endpoints that don't require authentication where you want to limit by the specific resource being accessed.
Note
- Does not support rate limit overrides.
- Does not support "monitor" mode (treated as disabled).
ProjectRateLimiter
dataclass
ProjectRateLimiter(
max_requests, expiry=1, policy_name="requests"
)
Bases: RateLimiter
Applies rate limits on the endpoint, for a project.
Supports per-project, per-endpoint rate limit overrides stored in Redis.
Use set_rate_limit_override() to configure overrides.
The override lookup and rate limiting are performed atomically in a single Lua script for efficiency.
make_key
make_key(request)
Generate the Redis key for rate limiting.
Source code in application_kit/fastapi/ratelimit.py
211 212 213 214 215 216 217 | |
make_override_key
make_override_key(request)
Generate the Redis key for looking up rate limit overrides.
Source code in application_kit/fastapi/ratelimit.py
219 220 221 222 223 224 225 | |
make_org_override_key
make_org_override_key(request)
Generate the Redis key for looking up organization-level rate limit overrides.
Source code in application_kit/fastapi/ratelimit.py
227 228 229 230 231 232 233 | |
is_rate_limit_disabled
is_rate_limit_disabled()
Check if rate limiting should be skipped for rate limiters that don't support monitor mode.
Returns True when RATE_LIMIT_MODE is "off" or "monitor". Rate limiters that
don't implement monitor mode (e.g., PathRateLimiter, custom RateLimiter
subclasses) should treat monitor mode as disabled — silently skipping rather
than incorrectly blocking requests.
Only ProjectRateLimiter and apply_rate_limit support monitor mode (they
check get_rate_limit_mode() directly and tag Datadog spans instead of blocking).
Example
class IpRateLimiter(RateLimiter):
def make_key(self, request: Request) -> str | None:
if is_rate_limit_disabled():
return None
ip = request.headers.get("X-Forwarded-For", "unknown")
return make_path_rate_limit_key(f"{request.url.path}:{ip}")
Source code in application_kit/ratelimit.py
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 | |
apply_rate_limit
async
apply_rate_limit(
request,
response,
max_requests,
expiry,
redis_client,
increment_amount=1,
key_suffix="",
policy_name=None,
)
Apply rate limiting programmatically (not via Depends).
Use this when rate limit parameters depend on runtime logic, such as different limits based on request content.
| PARAMETER | DESCRIPTION |
|---|---|
request
|
The incoming FastAPI request.
TYPE:
|
response
|
The FastAPI response to update headers on.
TYPE:
|
max_requests
|
Maximum requests allowed in the time window (must be >= 1).
TYPE:
|
expiry
|
Time window in seconds (must be >= 1).
TYPE:
|
redis_client
|
Async Redis connection for rate limiting.
TYPE:
|
increment_amount
|
Amount to increment the counter by (default: 1, must be >= 1). Use values > 1 for element-based rate limiting.
TYPE:
|
key_suffix
|
Optional suffix to append to the rate limit key. Use to separate different rate limit counters for the same endpoint.
TYPE:
|
policy_name
|
IETF policy name for structured headers. When None, auto-derived from key_suffix (e.g. "" → "requests").
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If max_requests, expiry, or increment_amount is less than 1. |
HTTPException
|
429 status when over limit. |
Source code in application_kit/fastapi/ratelimit.py
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | |
apply_element_rate_limit
async
apply_element_rate_limit(
request,
response,
max_requests,
expiry,
increment_amount,
redis_client,
key_suffix="/elements",
policy_name=None,
)
Apply element-based rate limiting.
Convenience wrapper for apply_rate_limit with element-specific defaults. Used for endpoints where rate limiting is based on the number of elements (e.g., matrix origins × destinations) rather than just request count.
| PARAMETER | DESCRIPTION |
|---|---|
request
|
The incoming FastAPI request.
TYPE:
|
response
|
The FastAPI response to update headers on.
TYPE:
|
max_requests
|
Maximum elements allowed in the time window.
TYPE:
|
expiry
|
Time window in seconds.
TYPE:
|
increment_amount
|
Number of elements in this request.
TYPE:
|
redis_client
|
Async Redis connection for rate limiting.
TYPE:
|
key_suffix
|
Suffix to append to the rate limit key (default: "/elements"). Separates element limits from request count limits.
TYPE:
|
policy_name
|
IETF policy name for structured headers. When None, auto-derived from key_suffix (e.g. "/elements" → "elements").
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
HTTPException
|
429 status when over limit. |
Source code in application_kit/fastapi/ratelimit.py
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 | |
Django
application_kit.django.ratelimit
RateLimitExceeded
RateLimitExceeded(result)
Bases: HttpException
Raised when rate limit is exceeded.
Source code in application_kit/django/ratelimit.py
38 39 40 | |
rate_limit
rate_limit(
max_requests,
expiry=1,
endpoint_name=None,
redis_dependency="ratelimit",
policy_name="requests",
)
Rate limit decorator for Django views.
Must be placed after authenticate_key or authenticate_user decorator
to have access to the project token.
| PARAMETER | DESCRIPTION |
|---|---|
max_requests
|
Maximum requests allowed in the time window (must be >= 1).
TYPE:
|
expiry
|
Time window in seconds (default: 1 second, must be >= 1).
TYPE:
|
endpoint_name
|
Optional identifier for the endpoint. If not provided,
automatically extracted from the request's URL pattern
(e.g., '/api/v1/datasets/
TYPE:
|
redis_dependency
|
Name of the Redis dependency in bender config.
TYPE:
|
policy_name
|
IETF policy name for structured headers (default: "requests").
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If max_requests or expiry is less than 1. |
Example
@authenticate_key() @rate_limit(max_requests=100, expiry=60) def my_view(request): return JsonResponse({"status": "ok"})
Or with explicit endpoint name:
@authenticate_key() @rate_limit(max_requests=100, expiry=60, endpoint_name="custom_name") def my_view(request): return JsonResponse({"status": "ok"})
Rate limit overrides can be configured per project using the async functions
from application_kit.fastapi.ratelimit:
- set_rate_limit_override() to create/update an override
- get_rate_limit_override() to retrieve an override
- delete_rate_limit_override() to remove an override
- list_rate_limit_overrides() to list all overrides for a project
Override key format: ratelimit_override:{endpoint_name}:{org_id}:{project_id}
Source code in application_kit/django/ratelimit.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | |