Rate Limiting
Application Kit provides rate limiting with support for per-project, per-endpoint limits and Redis-backed overrides at both the project and organization level.
Overview
Rate limiting protects your endpoints from abuse by limiting the number of requests a project can make within a time window. Features include:
- Per-project, per-endpoint rate limits with configurable time windows
- Element-based counting (e.g., limit by matrix elements, not just requests)
- Rate limit overrides stored in Redis/Valkey — per-project and per-organization
- Monitor mode — count violations without blocking, with Datadog span tagging
- Programmatic API for runtime-determined limits (
apply_rate_limit) - IETF
RateLimit-PolicyandRateLimitstructured headers (draft-ietf-httpapi-ratelimit-headers-10)
flowchart LR
Request([Request]) --> Auth[Authentication]
Auth --> RL{Rate Limit<br/>Check}
RL -->|Under limit| Handler[Request Handler]
RL -->|Over limit| Mode{Mode?}
Mode -->|on| Reject[429 Too Many Requests]
Mode -->|monitor| Tag[Tag Datadog Span] --> Handler
Handler --> Response([Response + Headers])
Reject --> Headers([Response + Headers])
Authentication must come first
Rate limiting requires authentication to identify the project. Ensure authentication is applied before rate limiting in your decorator/dependency chain.
Usage
Use ProjectRateLimiter or PathRateLimiter as dependencies:
"""FastAPI rate limiting example."""
from fastapi import APIRouter, Depends
from application_kit.fastapi.ratelimit import PathRateLimiter, ProjectRateLimiter
from application_kit.fastapi.security import AuthenticateKey
# Use standard APIRouter - rate limit docs are auto-injected when included in app
router = APIRouter()
# Rate limit per project (requires authentication)
@router.get(
"/datasets/{dataset_id}/search",
dependencies=[
Depends(AuthenticateKey()), # Authentication first
Depends(ProjectRateLimiter(max_requests=100, expiry=60)), # Then rate limit
],
)
async def search_dataset(dataset_id: int) -> dict[str, str]:
"""Search within a dataset.
Rate limit info is automatically added to OpenAPI when using get_fastapi_app().
"""
return {"status": "ok"}
# Rate limit by path only (no authentication required)
@router.get(
"/public/endpoint",
dependencies=[Depends(PathRateLimiter(max_requests=100, expiry=60))],
)
async def public_endpoint() -> dict[str, str]:
"""Public endpoint with path-based rate limiting."""
return {"status": "ok"}
Use the @rate_limit decorator after authentication:
"""Django rate limiting example."""
from django.http import HttpRequest, JsonResponse
from application_kit.django.decorators import authenticate_key
from application_kit.django.ratelimit import rate_limit
@authenticate_key() # Authentication first (outermost)
@rate_limit(max_requests=100, expiry=60) # Then rate limit
def my_view(request: HttpRequest) -> JsonResponse:
return JsonResponse({"status": "ok"})
@authenticate_key()
@rate_limit(max_requests=100, expiry=60)
async def async_view(request: HttpRequest) -> JsonResponse:
return JsonResponse({"status": "ok"})
Use the Django @rate_limit decorator with Shinobi:
"""Django Ninja rate limiting example."""
from django.http import HttpRequest
from application_kit.django.decorators import authenticate_key
from application_kit.django.ratelimit import rate_limit
from application_kit.shinobi.api import WoosmapApi
from application_kit.shinobi.authentication import PublicKeyAuth
from application_kit.shinobi.decorators import chain, terminate
api = WoosmapApi(description="My API")
@api.get("/search", auth=[PublicKeyAuth()])
@chain(authenticate_key(), rate_limit(max_requests=100, expiry=60))
@terminate()
def search(request: HttpRequest) -> dict[str, str]:
return {"status": "ok"}
Choosing a Rate Limiter
ProjectRateLimiter (Recommended)
Use ProjectRateLimiter for most endpoints. It creates rate limit keys based on:
- Endpoint path pattern
- Organization ID
- Project ID
This ensures each project has its own rate limit counter, and supports per-project overrides.
PathRateLimiter (Limited Use Cases)
Use PathRateLimiter only when you don't have access to organization/project IDs. This applies to:
- Webhook endpoints where the authentication key doesn't provide project context (e.g., dataset reimport webhooks)
- Public endpoints that don't require authentication
PathRateLimiter limitations
PathRateLimiter creates a rate limit counter per actual request path. For example, /users/123 and /users/456 have separate counters. This means:
- Each unique URL path gets its own counter
- No per-project isolation
- Rate limit overrides are not supported
- Monitor mode is not supported (treated as disabled)
Only use this when ProjectRateLimiter isn't possible.
Parameters
These apply to ProjectRateLimiter, PathRateLimiter (FastAPI), and the @rate_limit decorator (Django):
| Parameter | Type | Default | Description |
|---|---|---|---|
max_requests |
int |
required | Maximum requests allowed in the time window |
expiry |
int |
1 |
Time window in seconds |
policy_name |
str |
"requests" |
IETF policy name for structured headers |
endpoint_name |
str |
auto-detected | Identifier for the endpoint (Django only) |
redis_dependency |
str |
"ratelimit" |
Name of the Redis dependency (Django only) |
For the programmatic API (apply_rate_limit), see Programmatic Rate Limiting.
Response Headers
All rate-limited responses include IETF structured headers per draft-ietf-httpapi-ratelimit-headers-10:
| Header | Description |
|---|---|
RateLimit-Policy |
Declared policies with quota and window — e.g. "requests";q=100;w=60 |
RateLimit |
Current status with remaining quota and time — e.g. "requests";r=82;t=45 |
Each rate limiter appends its policy as a comma-separated list member, so endpoints with multiple limiters (e.g. request + element counting) show all policies in a single header:
RateLimit-Policy: "requests";q=100;w=60, "elements";q=5000;w=60
RateLimit: "requests";r=82;t=45, "elements";r=4200;t=30
Policy names are auto-derived from the key suffix ("" → "requests", "/elements" → "elements"), or can be set explicitly via the policy_name parameter.
When a 429 is returned, the response carries all accumulated policy headers from prior limiters — not just the one that triggered the rejection.
Legacy Headers
Legacy RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers are automatically injected alongside the IETF structured headers for backward compatibility:
- FastAPI:
get_fastapi_app()includesLegacyRateLimitHeaderMiddlewareby default. Passuse_x_ratelimit_prefix=Trueto emitX-RateLimit-*headers instead. - Django:
ApplicationKitApiMiddlewareinjects legacy headers viaprocess_response. SetRATELIMIT_USE_X_PREFIX = Truein Django settings to emitX-RateLimit-*headers instead.
The legacy headers are derived from the last policy in the structured header, matching the old behavior where each limiter overwrote previous values.
Pending removal
LegacyRateLimitHeaderMiddleware and the Django process_response legacy header injection are pending removal. Migrate consumers to the IETF structured headers (RateLimit-Policy, RateLimit).
Rate Limit Exceeded
When the rate limit is exceeded, a 429 Too Many Requests response is returned with RateLimit-Policy and RateLimit headers.
FastAPI returns an HTTPException with status code 429 and the message "Rate limit exceeded. Try again later."
When using ApplicationKitApiMiddleware, the RateLimitExceeded exception is automatically handled and returns a JSON response:
{"detail": "Rate limit exceeded. Try again later."}
When using WoosmapApi, the RateLimitExceeded exception is automatically handled and returns a JSON response:
{"detail": "Rate limit exceeded. Try again later."}
Rate Limit Overrides
Override default rate limits without code changes. Overrides are stored in Redis and checked atomically with rate limiting. Two levels are supported:
- Project overrides — per-project, per-endpoint limits
- Organization overrides — per-org, per-endpoint limits (fallback when no project override exists)
The priority chain is: project override → org override → defaults.
Only works with ProjectRateLimiter
Overrides only apply to endpoints using ProjectRateLimiter. Custom rate limiters (like IP-based limiters) do not support overrides.
Internal use only
The override management functions are provided for use by dedicated provisioning services. Individual applications should not manage overrides directly—this responsibility belongs to a centralized service that handles rate limit configuration across all projects.
Project Override Functions
"""Rate limit override management examples."""
import redis.asyncio
from application_kit.fastapi.ratelimit import (
clear_project_rate_limit_overrides,
delete_rate_limit_override,
get_rate_limit_override,
list_rate_limit_overrides,
set_rate_limit_override,
)
async def grant_higher_limit(redis: "redis.asyncio.Redis[bytes]", organization_id: int, project_id: int) -> None:
await set_rate_limit_override(
redis,
organization_id=organization_id,
project_id=project_id,
endpoint_pattern="/datasets/{dataset_id}/search", # FastAPI format
max_requests=1000,
expiry=60,
override_ttl=86400, # Optional: auto-expire after 24 hours
)
async def manage_overrides(redis: "redis.asyncio.Redis[bytes]") -> None:
# Get a specific override
override = await get_rate_limit_override(redis, organization_id=1, project_id=2, endpoint_pattern="/api/search")
if override:
print(f"Limit: {override.max_requests}, Window: {override.expiry}s")
# Delete a specific override
deleted = await delete_rate_limit_override(redis, organization_id=1, project_id=2, endpoint_pattern="/api/search")
print(f"Deleted: {deleted}")
# List all overrides for a project
overrides = await list_rate_limit_overrides(redis, organization_id=1, project_id=2)
for endpoint, ovr in overrides:
print(f"{endpoint}: {ovr.max_requests} req/{ovr.expiry}s")
# Clear all overrides for a project
count = await clear_project_rate_limit_overrides(redis, organization_id=1, project_id=2)
print(f"Cleared {count} overrides")
Organization Override Functions
Org overrides set limits for all projects in an organization at once. Each project still gets its own counter — the org override only defines what the limit is, not a shared bucket. For example, if an org override sets 100 req/min, project A and project B each get 100 req/min independently.
"""Organization-level rate limit override management examples."""
import redis.asyncio
from application_kit.fastapi.ratelimit import (
clear_org_rate_limit_overrides,
delete_org_rate_limit_override,
get_org_rate_limit_override,
list_org_rate_limit_overrides,
set_org_rate_limit_override,
)
async def grant_org_higher_limit(redis: "redis.asyncio.Redis[bytes]", organization_id: int) -> None:
"""Set a rate limit override for all projects in an organization."""
await set_org_rate_limit_override(
redis,
organization_id=organization_id,
endpoint_pattern="/datasets/{dataset_id}/search", # FastAPI format
max_requests=500,
expiry=60,
override_ttl=86400, # Optional: auto-expire after 24 hours
)
async def manage_org_overrides(redis: "redis.asyncio.Redis[bytes]") -> None:
# Get a specific org override
override = await get_org_rate_limit_override(redis, organization_id=1, endpoint_pattern="/api/search")
if override:
print(f"Limit: {override.max_requests}, Window: {override.expiry}s")
# Delete a specific org override
deleted = await delete_org_rate_limit_override(redis, organization_id=1, endpoint_pattern="/api/search")
print(f"Deleted: {deleted}")
# List all overrides for an organization
overrides = await list_org_rate_limit_overrides(redis, organization_id=1)
for endpoint, ovr in overrides:
print(f"{endpoint}: {ovr.max_requests} req/{ovr.expiry}s")
# Clear all overrides for an organization
count = await clear_org_rate_limit_overrides(redis, organization_id=1)
print(f"Cleared {count} overrides")
Note
The endpoint_pattern format depends on your framework:
- FastAPI:
/datasets/{dataset_id}/search - Django:
/api/v1/datasets/<int:dataset_id>/search
Temporary Overrides
Use the override_ttl parameter to create overrides that automatically expire. This works for both project and org overrides:
"""Temporary rate limit override examples."""
import redis.asyncio
from application_kit.ratelimit_overrides import set_org_rate_limit_override, set_rate_limit_override
async def temporary_project_override(redis: "redis.asyncio.Redis[bytes]") -> None:
await set_rate_limit_override(
redis,
organization_id=1,
project_id=2,
endpoint_pattern="/api/v1/search",
max_requests=1000,
expiry=60,
override_ttl=3600, # Override expires after 1 hour
)
async def temporary_org_override(redis: "redis.asyncio.Redis[bytes]") -> None:
await set_org_rate_limit_override(
redis,
organization_id=1,
endpoint_pattern="/api/v1/search",
max_requests=500,
expiry=60,
override_ttl=86400, # Override expires after 24 hours
)
Use cases for temporary overrides:
- Promotions: Increase limits during marketing events
- Trial periods: Grant premium rate limits temporarily
- Incident response: Temporarily raise limits while investigating issues
Programmatic Rate Limiting (FastAPI)
Use apply_rate_limit when rate limit parameters aren't known at import time — e.g., limits that vary by request body, or element-based counting where the increment depends on the payload size.
"""FastAPI programmatic rate limiting example."""
from typing import Annotated
import redis.asyncio
from fastapi import APIRouter, Depends
from starlette.requests import Request
from starlette.responses import Response
from application_kit.fastapi.ratelimit import apply_element_rate_limit, apply_rate_limit
from application_kit.fastapi.redis import RedisDependency
from application_kit.fastapi.security import AuthenticateKey
router = APIRouter(dependencies=[Depends(AuthenticateKey())])
@router.post("/geocode/batch")
async def batch_geocode(
request: Request,
response: Response,
ratelimit_redis: Annotated["redis.asyncio.Redis[bytes]", Depends(RedisDependency("ratelimit"))],
) -> dict[str, str]:
addresses = await request.json()
# Standard rate limit: 100 requests per minute
await apply_rate_limit(request, response, max_requests=100, expiry=60, redis_client=ratelimit_redis)
# Element-based rate limit: 1000 addresses per minute (separate counter)
await apply_element_rate_limit(
request, response, max_requests=1000, expiry=60, increment_amount=len(addresses), redis_client=ratelimit_redis
)
return {"status": "ok"}
Element-Based Counting
apply_element_rate_limit is a convenience wrapper for counting elements instead of requests. For example, a geocoding batch endpoint might allow 1000 addresses per minute regardless of how many requests that takes.
The Lua script checks current + increment_amount <= max_requests before incrementing, so a request that would cross the boundary is blocked — no overshoot. For example, at 95/100 with increment_amount=10, the request is rejected because 95 + 10 > 100.
It uses key_suffix="/elements" by default to keep element counters separate from request counters on the same endpoint. You can use a custom key_suffix on apply_rate_limit to create additional independent counters (e.g., key_suffix="/heavy" for expensive operations).
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
request |
Request |
required | The incoming FastAPI request |
response |
Response |
required | The FastAPI response (headers are set on this) |
max_requests |
int |
required | Maximum count allowed in the time window |
expiry |
int |
required | Time window in seconds |
redis_client |
Redis |
required | Async Redis/Valkey connection |
increment_amount |
int |
1 |
Amount to increment per call (>1 for element-based) |
key_suffix |
str |
"" |
Suffix for separate counters on the same endpoint |
policy_name |
str \| None |
None |
IETF policy name. When None, auto-derived from key_suffix |
Custom Rate Limiters (FastAPI)
For advanced use cases (e.g., rate limiting by IP instead of project), subclass RateLimiter and implement make_key():
"""Custom rate limiter example — rate limit by client IP address."""
from fastapi import APIRouter, Depends
from starlette.requests import Request
from application_kit.authenticator.utils import extract_user_ip_from_headers
from application_kit.fastapi.ratelimit import RateLimiter, is_rate_limit_disabled
from application_kit.ratelimit import make_path_rate_limit_key
class IpRateLimiter(RateLimiter):
"""Rate limit by client IP address instead of project."""
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}")
router = APIRouter()
@router.get(
"/public-endpoint",
dependencies=[Depends(IpRateLimiter(max_requests=50, expiry=1))],
)
async def public_endpoint() -> dict[str, str]:
return {"status": "ok"}
Return None to skip rate limiting
If make_key() returns None, rate limiting is skipped for that request. This is useful for disabling rate limits in development or for certain request types.
Overrides not supported for custom rate limiters
Rate limit overrides (via set_rate_limit_override()) only work with ProjectRateLimiter. Custom rate limiters like IpRateLimiter cannot be overridden because they don't use project-based keys.
Automatic Endpoint Detection
ProjectRateLimiter uses request.scope['route'].path automatically:
# Route: /datasets/{dataset_id}/search
# Rate limit key: ratelimit:/datasets/{dataset_id}/search:{org_id}:{project_id}
When endpoint_name is not provided, the decorator extracts the route pattern from Django's resolver_match:
"""Django automatic endpoint detection example."""
from django.http import HttpRequest, JsonResponse
from application_kit.django.decorators import authenticate_key
from application_kit.django.ratelimit import rate_limit
# URL pattern: path('api/v1/datasets/<int:dataset_id>/search', search_view)
@authenticate_key()
@rate_limit(max_requests=100, expiry=60) # Auto-detects: "/api/v1/datasets/<int:dataset_id>/search"
def search_view(request: HttpRequest, dataset_id: int) -> JsonResponse:
return JsonResponse({"results": []})
Implementation Details
Rate limiting uses Redis with Lua scripts for atomic operations:
- Atomic counting: Increment and check happen in a single Redis operation
- Override lookup: Override configuration is fetched and applied atomically
- TTL management: Rate limit windows are automatically managed via Redis TTL
flowchart TD
Request([Request]) --> A
subgraph LuaScript ["Lua Script (Atomic)"]
A[Check Project Override] --> B{Project override<br/>exists?}
B -->|Yes| C[Use project override limits]
B -->|No| D{Org override<br/>exists?}
D -->|Yes| C2[Use org override limits]
D -->|No| E2[Use default limits]
C --> F[Get current count]
C2 --> F
E2 --> F
F --> G{Count >= Limit?}
G -->|No| H[Increment counter]
G -->|Yes| I[Over limit]
H --> J{First request?}
J -->|Yes| K[Set TTL on key]
J -->|No| L[Return count]
K --> L
end
subgraph Responses [" "]
Response429([429 + Headers])
Response200([200 + Headers])
end
I --> Response429
L --> Response200
Redis key format:
- Rate limit counter:
ratelimit:{endpoint}:{organization_id}:{project_id} - Project override:
ratelimit_override:{endpoint}:{organization_id}:{project_id} - Org override:
ratelimit_org_override:{endpoint}:{organization_id}
Configuration
Rate Limit Mode
Rate limiting behavior is controlled by the RATE_LIMIT_MODE environment variable:
| Mode | Django | FastAPI (ProjectRateLimiter) |
FastAPI (PathRateLimiter) |
Description |
|---|---|---|---|---|
on |
Yes | Yes | Yes | Rate limiting is enabled and enforced (default) |
off |
Yes | Yes | Yes | Rate limiting is completely disabled |
monitor |
Yes | Yes | Disabled | Requests are counted but not blocked; tags Datadog spans |
Set the mode via environment variable:
export RATE_LIMIT_MODE=on # or "off" or "monitor"
Monitor mode support
Monitor mode is supported by Django and FastAPI's ProjectRateLimiter. The PathRateLimiter and custom rate limiters using the base RateLimiter class do not support monitor mode — when set to monitor, rate limiting is disabled (same as off). To support monitor mode in custom rate limiters, override __call__ and check get_rate_limit_mode() directly.
Redis Dependency
Add the ratelimit Redis dependency to your application.json:
{
"dependencies": {
"databases": [
{"name": "ratelimit", "type": "redis"}
]
}
}
OpenAPI Documentation
When using get_fastapi_app(), rate limit metadata is automatically added to the OpenAPI schema when you include routers:
- Machine-readable:
x-ratelimit-limitandx-ratelimit-window-secondsfields - Human-readable: "Ratelimit: X/Ys" appended to the endpoint description
from fastapi import APIRouter, Depends
from application_kit.fastapi.fastapi import get_fastapi_app
from application_kit.fastapi.ratelimit import ProjectRateLimiter
app = get_fastapi_app("myapi")
router = APIRouter()
@router.get("/search", dependencies=[Depends(ProjectRateLimiter(max_requests=100, expiry=60))])
async def search():
"""Search for items.""" # Becomes "Search for items.\n\n**Ratelimit:** 100/60s"
return {"status": "ok"}
app.include_router(router) # Rate limit docs auto-injected here
The generated OpenAPI will include:
{
"paths": {
"/search": {
"get": {
"description": "Search for items.\n\n**Ratelimit:** 100/60s",
"x-ratelimit-limit": 100,
"x-ratelimit-window-seconds": 60
}
}
}
}
Django does not automatically generate OpenAPI schemas. Document rate limits in your API documentation manually.
API Reference
application_kit.fastapi.ratelimit
set_rate_limit_override
module-attribute
set_rate_limit_override = _warn(set_rate_limit_override)
get_rate_limit_override
module-attribute
get_rate_limit_override = _warn(get_rate_limit_override)
delete_rate_limit_override
module-attribute
delete_rate_limit_override = _warn(
delete_rate_limit_override
)
list_rate_limit_overrides
module-attribute
list_rate_limit_overrides = _warn(list_rate_limit_overrides)
clear_project_rate_limit_overrides
module-attribute
clear_project_rate_limit_overrides = _warn(
clear_project_rate_limit_overrides
)
set_org_rate_limit_override
module-attribute
set_org_rate_limit_override = _warn(
set_org_rate_limit_override
)
get_org_rate_limit_override
module-attribute
get_org_rate_limit_override = _warn(
get_org_rate_limit_override
)
delete_org_rate_limit_override
module-attribute
delete_org_rate_limit_override = _warn(
delete_org_rate_limit_override
)
list_org_rate_limit_overrides
module-attribute
list_org_rate_limit_overrides = _warn(
list_org_rate_limit_overrides
)
clear_org_rate_limit_overrides
module-attribute
clear_org_rate_limit_overrides = _warn(
clear_org_rate_limit_overrides
)
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 | |
RateLimitOverride
Bases: BaseModel
Rate limit override configuration stored in Redis.
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 | |
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 | |