Throttling¶
django-modern-rest ships its own throttling
(also known as “rate limiting”) mechanism.
Here’s how everything works.
Important
If you have an option not to use ratelimiting in Django, but to use it on the HTTP Proxy side, you should prefer the proxy. It is significantly faster and more secure.
Defining throttling¶
We have two classes to define throttling:
dmr.throttling.SyncThrottlefor sync endpointsdmr.throttling.AsyncThrottlefor async endpoints
We can define throttling on three different levels:
1from dmr import Controller, modify
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.throttling import Rate, SyncThrottle
4
5
6class SyncController(Controller[PydanticSerializer]):
7 @modify(throttling=[SyncThrottle(1, Rate.minute)])
8 def get(self) -> str:
9 return 'inside'
10
Run result
$ curl http://127.0.0.1:8000/api/sync/ -X GET
"inside"
$ curl http://127.0.0.1:8000/api/sync/ -D - -X GET
HTTP/1.1 429 Too Many Requests
date: Fri, 05 Jun 2026 12:23:01 GMT
server: uvicorn
X-RateLimit-Limit: 1
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60
Retry-After: 60
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Too many requests","type":"ratelimit"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/synccontroller/": {
"get": {
"deprecated": false,
"operationId": "getSynccontrollerApiSynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"Retry-After": {
"description": "Indicates how long the user agent should wait before making a follow-up request",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Limit": {
"description": "The maximum number of requests permitted in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Remaining": {
"description": "The number of requests remaining in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Reset": {
"description": "The number of seconds until the current rate limit window resets",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.throttling import AsyncThrottle, Rate
4
5
6class AsyncController(Controller[PydanticSerializer]):
7 throttling = (AsyncThrottle(1, Rate.minute),)
8
9 async def get(self) -> str:
10 return 'inside'
11
Run result
$ curl http://127.0.0.1:8000/api/async/ -X GET
"inside"
$ curl http://127.0.0.1:8000/api/async/ -D - -X GET
HTTP/1.1 429 Too Many Requests
date: Fri, 05 Jun 2026 12:23:02 GMT
server: uvicorn
X-RateLimit-Limit: 1
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 59
Retry-After: 59
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Too many requests","type":"ratelimit"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/asynccontroller/": {
"get": {
"deprecated": false,
"operationId": "getAsynccontrollerApiAsynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"Retry-After": {
"description": "Indicates how long the user agent should wait before making a follow-up request",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Limit": {
"description": "The maximum number of requests permitted in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Remaining": {
"description": "The number of requests remaining in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Reset": {
"description": "The number of seconds until the current rate limit window resets",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
1>>> from dmr.settings import Settings
2>>> from dmr.throttling import SyncThrottle, Rate
3
4>>> DMR_SETTINGS = {Settings.throttling: [SyncThrottle(5, Rate.minute)]}
Providing several throttling instances means that all of them must succeed. When multiple throttling rules are defined on different levels, their rules are joined.
For example:
1from dmr import Controller, modify
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.throttling import Rate, SyncThrottle
4
5
6class SyncController(Controller[PydanticSerializer]):
7 throttling = (SyncThrottle(5, Rate.hour),)
8
9 @modify(throttling=[SyncThrottle(1, Rate.minute)])
10 def get(self) -> str:
11 return 'inside'
12
Run result
$ curl http://127.0.0.1:8000/api/sync/ -X GET
"inside"
$ curl http://127.0.0.1:8000/api/sync/ -D - -X GET
HTTP/1.1 429 Too Many Requests
date: Fri, 05 Jun 2026 12:23:02 GMT
server: uvicorn
X-RateLimit-Limit: 1
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 59
Retry-After: 59
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Too many requests","type":"ratelimit"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/synccontroller/": {
"get": {
"deprecated": false,
"operationId": "getSynccontrollerApiSynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"Retry-After": {
"description": "Indicates how long the user agent should wait before making a follow-up request",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Limit": {
"description": "The maximum number of requests permitted in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Remaining": {
"description": "The number of requests remaining in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Reset": {
"description": "The number of seconds until the current rate limit window resets",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
Will guard GET method with 2 throttling checks:
Not more
<=than 1 request per minuteAnd not more
<=than 5 requests per hour
Customizing throttling¶
Rates¶
Rate is passed as the second required
parameter to throttle classes. However, all values that you pass
are just numbers of seconds. So, you can fully customize throttling
timings by passing any amount of seconds that you wish:
1>>> from dmr.settings import Settings, DMR_SETTINGS
2>>> from dmr.throttling import SyncThrottle
3
4>>> DMR_SETTINGS = {
5... Settings.throttling: [
6... SyncThrottle(
7... max_requests=5,
8... duration_in_seconds=10,
9... ),
10... ],
11... }
This will set a throttling rule: no more than 5 requests in 10 seconds.
Backends¶
Backends are used to define where we store throttling data.
By default we use:
dmr.throttling.backends.SyncDjangoCachefor sync endpointsdmr.throttling.backends.AsyncDjangoCachefor async endpoints
All backends that we support can be further customized.
By default we store all the data in the 'default' Django cache.
You can customize which Django cache name is used. For example:
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.throttling import Rate, SyncThrottle
4from dmr.throttling.backends import SyncDjangoCache
5
6
7class SyncController(Controller[PydanticSerializer]):
8 throttling = (
9 SyncThrottle(
10 max_requests=1,
11 duration_in_seconds=Rate.minute,
12 backend=SyncDjangoCache(cache_name='throttling'),
13 ),
14 )
15
16 def get(self) -> str:
17 return 'inside'
18
Run result
$ curl http://127.0.0.1:8000/api/sync/ -X GET
"inside"
$ curl http://127.0.0.1:8000/api/sync/ -D - -X GET
HTTP/1.1 429 Too Many Requests
date: Fri, 05 Jun 2026 12:23:03 GMT
server: uvicorn
X-RateLimit-Limit: 1
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60
Retry-After: 60
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Too many requests","type":"ratelimit"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/synccontroller/": {
"get": {
"deprecated": false,
"operationId": "getSynccontrollerApiSynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"Retry-After": {
"description": "Indicates how long the user agent should wait before making a follow-up request",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Limit": {
"description": "The maximum number of requests permitted in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Remaining": {
"description": "The number of requests remaining in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Reset": {
"description": "The number of seconds until the current rate limit window resets",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
Any Redis-compliant tool is supported, including: Valkey, KeyDB, etc.
You can fully customize the client:
1from typing import Any
2
3import redis
4from django.core.cache import caches
5
6from dmr import Controller
7from dmr.plugins.pydantic import PydanticSerializer
8from dmr.throttling import Rate, SyncThrottle
9from dmr.throttling.backends.redis import SyncRedis
10
11# If `CACHES['redis']` with `django.core.cache.backends.redis.RedisCache`
12# backend is defined in `settings.py`:
13redis_client: 'redis.Redis[Any]' = caches['redis']._cache.get_client() # type: ignore[attr-defined] # noqa: SLF001
14
15
16class SyncController(Controller[PydanticSerializer]):
17 throttling = (
18 SyncThrottle(
19 max_requests=1,
20 duration_in_seconds=Rate.minute,
21 backend=SyncRedis(redis_client),
22 ),
23 )
24
25 def get(self) -> str:
26 return 'inside'
Note
Make sure that redis client
library is installed, we don’t ship
it together with django-modern-rest.
You can also write your own backends, for example,
to store throttling information in memory, filesystem, or somewhere else.
To do so, you would need to subclass
dmr.throttling.backends.BaseThrottleSyncBackend
or dmr.throttling.backends.BaseThrottleAsyncBackend
and override 2 methods.
Full list of backends that we ship in django-modern-rest:
SyncDjangoCacheandAsyncDjangoCache, defaultSyncRedisandAsyncRedis
Warning
When using SyncDjangoCache
or AsyncDjangoCache
the final behavior will depend on the cache that you use.
Some Django cache backends like
django.core.cache.backends.locmem.LocMemCache
store cache in memory per-process. So, any multiprocess environments
with N processes will allow to use N * max_request requests.
Using such cache backends is not safe.
Some like django.core.cache.backends.dummy.DummyCache do nothing at all.
Choosing a backend¶
Backend |
Atomicity |
Overhead |
Supported algorithms |
Best suited for |
|---|---|---|---|---|
|
Per-process: multiprocess deployments may face problems |
Very low (depends on the cache type) |
All |
Non-critical IP based checks with not-strict windows and limits |
|
Full |
Low |
All builtin ones, but requires |
Strict distributed limits |
Unsafe backend warning¶
By default, django-modern-rest emits
a UnsafeCacheBackendWarning
warning when detecting an unsafe cache backend for throttling.
You can configure this check on three levels as usual:
Per endpoint: pass
throttling_allow_unsafe_cacheparameterPer controller: by setting
throttling_allow_unsafe_cacheattributeIn settings, see
throttling_allow_unsafe_cache
When throttling_allow_unsafe_cache is set to False,
we raise a dmr.exceptions.EndpointMetadataError
exception instead of a warning. This setting will ensure the maximum safety.
To suppress this check completely and run throttling at your own risk,
set throttling_allow_unsafe_cache to None.
Algorithms¶
Algorithms are used to define the logic of how requests are counted.
By default we use dmr.throttling.algorithms.SimpleRate
as the algorithm.
It defines a fixed window with a fixed amount of requests possible.
When window is expired, it resets the count of requests.
Here’s how you can customize the algorithm for a throttling:
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.throttling import Rate, SyncThrottle
4from dmr.throttling.algorithms import LeakyBucket
5
6
7class SyncController(Controller[PydanticSerializer]):
8 throttling = (
9 SyncThrottle(
10 max_requests=1,
11 duration_in_seconds=Rate.minute,
12 algorithm=LeakyBucket(),
13 ),
14 )
15
16 def get(self) -> str:
17 return 'inside'
18
Run result
$ curl http://127.0.0.1:8000/api/sync/ -X GET
"inside"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/synccontroller/": {
"get": {
"deprecated": false,
"operationId": "getSynccontrollerApiSynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"Retry-After": {
"description": "Indicates how long the user agent should wait before making a follow-up request",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Limit": {
"description": "The maximum number of requests permitted in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Remaining": {
"description": "The number of requests remaining in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Reset": {
"description": "The number of seconds until the current rate limit window resets",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
You can also write your own algorithms.
To do so, you would need to subclass
dmr.throttling.algorithms.BaseThrottleAlgorithm
and override 2 required methods.
Full list of algorithms that we ship in django-modern-rest:
SimpleRate, defaultLeakyBucketwhere requests fill the bucket; tokens leak at a steady rate. UnlikeSimpleRate, drains continuously providing smoother rate-limiting without allowing bursts at window boundaries.
Warning
SimpleRate uses a fixed window
that resets when the window expires. This allows a boundary burst pattern:
a client can send N requests right before the window resets and N
more right after, effectively firing 2N requests in a short interval
while remaining within the configured per-window limit.
For abuse-sensitive endpoints (login, OTP, password reset) prefer
LeakyBucket, which drains
continuously and eliminates this burst window.
Choosing an algorithm¶
Algorithm |
Window type |
Overhead |
Boundary burst risk |
Best suited for |
|---|---|---|---|---|
Fixed — resets after |
Very low |
Yes — up to |
General-purpose, internal, and admin endpoints |
|
Continuous drain |
Low |
No — traffic is smoothed regardless of timing |
Auth endpoints (login, OTP, password reset), public APIs |
For auth and abuse-sensitive endpoints, use
LeakyBucket:
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticFastSerializer
3from dmr.throttling import Rate, SyncThrottle
4from dmr.throttling.algorithms import LeakyBucket
5
6
7class LoginController(Controller[PydanticFastSerializer]):
8 throttling = (
9 SyncThrottle(
10 max_requests=5,
11 duration_in_seconds=Rate.minute,
12 algorithm=LeakyBucket(),
13 ),
14 )
15
16 def post(self) -> str:
17 return 'logged in'
Cache keys¶
Cache keys is what defines how requests are identified.
By default we use dmr.throttling.cache_keys.RemoteAddr() cache key,
which identifies requests by IP taken from
REMOTE_ADDR
value from request.META.
Warning
If you are using reverse proxies, make sure to correctly configure
how they pass request headers, to REMOTE_ADDR would be correct.
You can write your own cache keys, they are subclasses
of BaseThrottleCacheKey
and must return a string or None.
If cache key returns None, it means that this request
will be skipped from this exact throttling check.
However, other keys may still be applied.
It is useful to skip some requests from throttling checks, for example, from paid or stuff users.
Full list of cache keys that we ship in django-modern-rest:
RemoteAddr, defaultUserPk, based onrequest.user, by default we userequest.user.pkif it exists. You can passexclude_stuffargument asFalseto also limitis_stuffusers, or you can passexclude_superuserargument asFalseto also limit super usersJwtToken, based onrequest.__dmr_jwt__. Usesjticlaim when present and falls back tosubclaim. Raw value is hashed before being used as a cache key. ReturnsNonewhenrequest.__dmr_jwt__is not set.
When throttling is executed¶
Throttling is executed in two stages: before auth and after auth. Why? Because we need to:
Protect auth from abusive requests and brute forcing
Make sure we can base throttling rules on the auth info
The same can be said about content negotiation, it also must be protected by throttling. Otherwise, people can abuse content negotiation without any request limits.
---
config:
theme: forest
---
graph
Start[New request] --> BeforeThrottle[Throttling based on IP or 429];
BeforeThrottle --> RendererNegotiation[Renderer is negotiated or 406];
RendererNegotiation --> Auth[Auth or 401];
Auth --> AfterThrottle[Throttling based on auth or 429];
Throttling execution¶
All cache keys know when to execute by default, however you can customize this. For example, you can run some IP based throttling checks after the auth itself:
1from dmr import Controller, modify
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.security.django_session import DjangoSessionSyncAuth
4from dmr.throttling import Rate, SyncThrottle
5from dmr.throttling.cache_keys import RemoteAddr
6
7
8class SyncController(Controller[PydanticSerializer]):
9 @modify(
10 auth=[DjangoSessionSyncAuth()],
11 throttling=[
12 SyncThrottle(
13 1,
14 Rate.minute,
15 cache_key=RemoteAddr(runs_before_auth=False),
16 ),
17 ],
18 )
19 def get(self) -> str:
20 return 'inside'
21
Run result
$ curl http://127.0.0.1:8000/api/sync/ -X GET
{"detail":[{"msg":"Not authenticated","type":"security"}]}
$ curl http://127.0.0.1:8000/api/sync/ -X GET
{"detail":[{"msg":"Not authenticated","type":"security"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {
"csrf": {
"description": "CSRF protection",
"in": "cookie",
"name": "csrftoken",
"type": "apiKey"
},
"django_session": {
"description": "Reusing standard Django auth flow for API",
"in": "cookie",
"name": "sessionid",
"type": "apiKey"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/synccontroller/": {
"get": {
"deprecated": false,
"operationId": "getSynccontrollerApiSynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when auth was not successful"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when CSRF check failed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"Retry-After": {
"description": "Indicates how long the user agent should wait before making a follow-up request",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Limit": {
"description": "The maximum number of requests permitted in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Remaining": {
"description": "The number of requests remaining in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Reset": {
"description": "The number of seconds until the current rate limit window resets",
"required": true,
"schema": {
"type": "string"
}
}
}
}
},
"security": [
{
"csrf": [],
"django_session": []
}
]
}
}
}
}
Warning
It is strongly not recommended to have auth without any throttling before it.
Auth must be protected from brute force and denial of service attacks! For example, one can also use django-axes for this.
wemake-django-template has this configured properly.
Note that it won’t make any sense to run auth-based throttling before auth. So, customize it with care.
Headers¶
By default on 429 Too Many Requests error we return four headers:
X-RateLimit-Limit- The maximum number of requests permitted in the current time windowX-RateLimit-Remaining- The number of requests remaining in the current time windowX-RateLimit-Reset- The number of seconds until the current rate limit window resetsRetry-After- The number of seconds until the current rate limit window resets, see RFC-6585 and RFC-7231
Note
Headers with X- prefix means that they are custom ones,
there’s no spec behind them.
However, this convention is the most popular one as of right now.
OpenAPI support is built in for this feature.
All headers classes will provide the proper
HeaderSpec for the 429 response.
You might want to customize the returned headers. To do so,
you can pass response_headers argument to throttling classes
with header classes that you actually want to support.
For example, you can disable Retry-After header with:
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.throttling import AsyncThrottle, Rate
4from dmr.throttling.headers import XRateLimit
5
6
7class AsyncController(Controller[PydanticSerializer]):
8 throttling = (
9 AsyncThrottle(1, Rate.minute, response_headers=[XRateLimit()]),
10 )
11
12 async def get(self) -> str:
13 return 'inside'
14
Run result
$ curl http://127.0.0.1:8000/api/async/ -X GET
"inside"
$ curl http://127.0.0.1:8000/api/async/ -D - -X GET
HTTP/1.1 429 Too Many Requests
date: Fri, 05 Jun 2026 12:23:06 GMT
server: uvicorn
X-RateLimit-Limit: 1
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Too many requests","type":"ratelimit"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/asynccontroller/": {
"get": {
"deprecated": false,
"operationId": "getAsynccontrollerApiAsynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"X-RateLimit-Limit": {
"description": "The maximum number of requests permitted in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Remaining": {
"description": "The number of requests remaining in the current time window",
"required": true,
"schema": {
"type": "string"
}
},
"X-RateLimit-Reset": {
"description": "The number of seconds until the current rate limit window resets",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
Or if you want to support
the latest draft
about RateLimit and RateLimit-Policy headers, you can use:
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.throttling import Rate, SyncThrottle
4from dmr.throttling.headers import RateLimitIETFDraft
5
6
7class SyncController(Controller[PydanticSerializer]):
8 throttling = (
9 SyncThrottle(1, Rate.minute, response_headers=[RateLimitIETFDraft()]),
10 )
11
12 def get(self) -> str:
13 return 'inside'
14
Run result
$ curl http://127.0.0.1:8000/api/sync/ -X GET
"inside"
$ curl http://127.0.0.1:8000/api/sync/ -D - -X GET
HTTP/1.1 429 Too Many Requests
date: Fri, 05 Jun 2026 12:23:07 GMT
server: uvicorn
RateLimit-Policy: 1;w=60;name="RemoteAddr"
RateLimit: "RemoteAddr";r=0;t=59
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Too many requests","type":"ratelimit"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/synccontroller/": {
"get": {
"deprecated": false,
"operationId": "getSynccontrollerApiSynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"RateLimit": {
"description": "Current rate limiting state",
"required": true,
"schema": {
"type": "string"
}
},
"RateLimit-Policy": {
"description": "Description of all rate limiting policies for this endpoint",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
You can also combine these headers with each other in any combinations.
You can write your own classes with custom headers support.
To do so, subclass dmr.throttling.headers.BaseResponseHeadersProvider.
You can completely disable any extra response headers by passing an empty list.
Full list of header providers that we ship in django-modern-rest:
XRateLimit, defaultRetryAfter, default
Security¶
Key considerations:
Be sure to correctly setup your HTTP Proxy server to send correct IP headers
Be especially careful with
X-Forwarded-Forheader, because it can contain several layers of proxiesNever rate limit on user supplied data such as
User-Agent, because this data can easily be changedDenial of service: be careful not to limit other users when limiting just one
Do not store sensitive or personal users’ data in your cache keys, because it is stored with no protection / encryption
Throttling reports¶
If you need to attach any throttling headers to successful responses, you can do it as well.
For this we offer two APIs:
dmr.throttling.ThrottlingReport.report()for sync APIsdmr.throttling.ThrottlingReport.areport()for async APIs
All our regular rules apply:
All new headers must be added to the corresponding
ResponseSpecdefinitionsWhen settings headers, you would need to use
validate()
1from http import HTTPStatus
2from typing import Final
3
4from django.http import HttpResponse
5
6from dmr import Controller, ResponseSpec, validate
7from dmr.plugins.pydantic import PydanticFastSerializer
8from dmr.throttling import AsyncThrottle, Rate, ThrottlingReport
9from dmr.throttling.cache_keys import RemoteAddr
10from dmr.throttling.headers import RateLimitIETFDraft
11
12_draft_headers: Final = RateLimitIETFDraft()
13
14
15class AsyncController(Controller[PydanticFastSerializer]):
16 @validate(
17 ResponseSpec(
18 str,
19 status_code=HTTPStatus.OK,
20 headers=_draft_headers.provide_headers_specs(),
21 ),
22 throttling=[
23 AsyncThrottle(
24 1,
25 Rate.second,
26 response_headers=[_draft_headers],
27 cache_key=RemoteAddr(name='per-second'),
28 ),
29 AsyncThrottle(
30 5,
31 Rate.minute,
32 response_headers=[_draft_headers],
33 cache_key=RemoteAddr(name='per-minute'),
34 ),
35 ],
36 )
37 async def get(self) -> HttpResponse:
38 return self.to_response(
39 'inside',
40 headers=await ThrottlingReport(self).areport(),
41 )
42
Run result
$ curl http://127.0.0.1:8000/api/async/ -D - -X GET
HTTP/1.1 200 OK
date: Fri, 05 Jun 2026 12:23:07 GMT
server: uvicorn
RateLimit-Policy: 1;w=1;name="per-second", 5;w=60;name="per-minute"
RateLimit: "per-second";r=0;t=1, "per-minute";r=4;t=60
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 8
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
"inside"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/asynccontroller/": {
"get": {
"deprecated": false,
"operationId": "getAsynccontrollerApiAsynccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK",
"headers": {
"RateLimit": {
"description": "Current rate limiting state",
"required": true,
"schema": {
"type": "string"
}
},
"RateLimit-Policy": {
"description": "Description of all rate limiting policies for this endpoint",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"RateLimit": {
"description": "Current rate limiting state",
"required": true,
"schema": {
"type": "string"
}
},
"RateLimit-Policy": {
"description": "Description of all rate limiting policies for this endpoint",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
Use headers argument to to_response()
to add needed headers.
Warning
ThrottlingReport will make N cache requests
when building header reports (where N is the number of throttle
instances used for this endpoint).
It might be slow, depending on the number of throttles and your cache.
It might also fail, we don’t handle any errors in the reports building process.
Use this feature only when there’s a serious need for it.
You can also provide the same headers not just for successful responses, but for any errors that consume ratelimit quota as well.
To so, you would need to customize headers spec of your error model:
1from collections.abc import Mapping
2from http import HTTPStatus
3from typing import Annotated, Any, Final
4
5import pydantic
6from django.http import HttpResponse
7from typing_extensions import override
8
9from dmr import Controller, NewCookie, Query, ResponseSpec, validate
10from dmr.errors import ErrorModel
11from dmr.metadata import ResponseSpecMetadata
12from dmr.plugins.pydantic import PydanticFastSerializer
13from dmr.renderers import Renderer
14from dmr.throttling import Rate, SyncThrottle, ThrottlingReport
15from dmr.throttling.cache_keys import RemoteAddr
16from dmr.throttling.headers import RateLimitIETFDraft
17
18_draft_headers: Final = RateLimitIETFDraft()
19
20
21class _QueryModel(pydantic.BaseModel):
22 number: int
23
24
25class SyncController(Controller[PydanticFastSerializer]):
26 error_model = Annotated[
27 ErrorModel,
28 ResponseSpecMetadata(headers=_draft_headers.provide_headers_specs()),
29 ]
30
31 @validate(
32 ResponseSpec(
33 str,
34 status_code=HTTPStatus.OK,
35 headers=_draft_headers.provide_headers_specs(),
36 ),
37 throttling=[
38 SyncThrottle(
39 2,
40 Rate.second,
41 response_headers=[_draft_headers],
42 cache_key=RemoteAddr(name='per-second'),
43 ),
44 SyncThrottle(
45 5,
46 Rate.minute,
47 response_headers=[_draft_headers],
48 cache_key=RemoteAddr(name='per-minute'),
49 ),
50 ],
51 )
52 def get(self, parsed_query: Query[_QueryModel]) -> HttpResponse:
53 return self.to_response('inside')
54
55 @override
56 def to_response(
57 self,
58 raw_data: Any,
59 *,
60 status_code: HTTPStatus | None = None,
61 headers: Mapping[str, str] | None = None,
62 cookies: Mapping[str, NewCookie] | None = None,
63 renderer: Renderer | None = None,
64 ) -> HttpResponse:
65 response_headers = ThrottlingReport(self).report()
66 response_headers.update(headers or {})
67 return super().to_response(
68 raw_data,
69 status_code=status_code,
70 headers=response_headers,
71 cookies=cookies,
72 renderer=renderer,
73 )
74
Run result
$ curl 'http://127.0.0.1:8000/api/sync/?number=1' -D - -X GET
HTTP/1.1 200 OK
date: Fri, 05 Jun 2026 12:23:08 GMT
server: uvicorn
RateLimit-Policy: 2;w=1;name="per-second", 5;w=60;name="per-minute"
RateLimit: "per-second";r=1;t=1, "per-minute";r=4;t=60
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 8
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
"inside"
$ curl 'http://127.0.0.1:8000/api/sync/?number=a' -D - -X GET
HTTP/1.1 400 Bad Request
date: Fri, 05 Jun 2026 12:23:08 GMT
server: uvicorn
RateLimit-Policy: 2;w=1;name="per-second", 5;w=60;name="per-minute"
RateLimit: "per-second";r=0;t=1, "per-minute";r=3;t=60
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 145
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Input should be a valid integer, unable to parse string as an integer","loc":["parsed_query","number"],"type":"value_error"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/synccontroller/": {
"get": {
"deprecated": false,
"operationId": "getSynccontrollerApiSynccontroller",
"parameters": [
{
"deprecated": false,
"in": "query",
"name": "number",
"required": true,
"schema": {
"title": "Number",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK",
"headers": {
"RateLimit": {
"description": "Current rate limiting state",
"required": true,
"schema": {
"type": "string"
}
},
"RateLimit-Policy": {
"description": "Description of all rate limiting policies for this endpoint",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed",
"headers": {
"RateLimit": {
"description": "Current rate limiting state",
"required": true,
"schema": {
"type": "string"
}
},
"RateLimit-Policy": {
"description": "Description of all rate limiting policies for this endpoint",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied",
"headers": {
"RateLimit": {
"description": "Current rate limiting state",
"required": true,
"schema": {
"type": "string"
}
},
"RateLimit-Policy": {
"description": "Description of all rate limiting policies for this endpoint",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema",
"headers": {
"RateLimit": {
"description": "Current rate limiting state",
"required": true,
"schema": {
"type": "string"
}
},
"RateLimit-Policy": {
"description": "Description of all rate limiting policies for this endpoint",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"429": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when throttling rate was hit",
"headers": {
"RateLimit": {
"description": "Current rate limiting state",
"required": true,
"schema": {
"type": "string"
}
},
"RateLimit-Policy": {
"description": "Description of all rate limiting policies for this endpoint",
"required": true,
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
Method to_response is used for both successful and error responses.
This way both your successful responses
and error responses will have the needed ratelimiting headers.
API Reference¶
Base¶
- class dmr.throttling.SyncThrottle(max_requests: int, duration_in_seconds: Rate | int, *, cache_key: BaseThrottleCacheKey | None = None, backend: _BackendT | None = None, algorithm: BaseThrottleAlgorithm | None = None, response_headers: Iterable[BaseResponseHeadersProvider] | None = None)[source]¶
Sync throttle type for sync endpoints.
Added in version 0.7.0.
- __call__(endpoint: Endpoint, controller: Controller[BaseSerializer], lock: AbstractContextManager[Any, Any]) None[source]¶
Put your throttle business logic here.
Return
Noneif throttle check passed. Raisedmr.exceptions.TooManyRequestsErrorif throttle check failed. Raisedmr.response.APIErrorif you want to change the return code, for example, when some data is missing or has wrong format.
- collect_response_headers(endpoint: Endpoint, controller: Controller[BaseSerializer], remaining: int, reset: int, *, report_all: bool = False) dict[str, str]¶
Collects response headers for all
response_headersclasses.
- full_cache_key(endpoint: Endpoint, controller: Controller[BaseSerializer]) str | None¶
Get the full cache key value.
- provide_response_specs(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], existing_responses: Mapping[HTTPStatus, ResponseSpec]) list[ResponseSpec]¶
Provides responses that can happen when throttle triggers.
- report_usage(endpoint: Endpoint, controller: Controller[BaseSerializer]) dict[str, str][source]¶
Report throttle usage stats.
- class dmr.throttling.AsyncThrottle(max_requests: int, duration_in_seconds: Rate | int, *, cache_key: BaseThrottleCacheKey | None = None, backend: _BackendT | None = None, algorithm: BaseThrottleAlgorithm | None = None, response_headers: Iterable[BaseResponseHeadersProvider] | None = None)[source]¶
Async throttle type for async endpoints.
Added in version 0.7.0.
- async __call__(endpoint: Endpoint, controller: Controller[BaseSerializer], lock: AbstractAsyncContextManager[Any, Any]) None[source]¶
Put your throttle business logic here.
Return
Noneif throttle check passed. Raisedmr.exceptions.TooManyRequestsErrorif throttle check failed. Raisedmr.response.APIErrorif you want to change the return code, for example, when some data is missing or has wrong format.
- collect_response_headers(endpoint: Endpoint, controller: Controller[BaseSerializer], remaining: int, reset: int, *, report_all: bool = False) dict[str, str]¶
Collects response headers for all
response_headersclasses.
- full_cache_key(endpoint: Endpoint, controller: Controller[BaseSerializer]) str | None¶
Get the full cache key value.
- provide_response_specs(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], existing_responses: Mapping[HTTPStatus, ResponseSpec]) list[ResponseSpec]¶
Provides responses that can happen when throttle triggers.
- async report_usage(endpoint: Endpoint, controller: Controller[BaseSerializer]) dict[str, str][source]¶
Async report throttle usage stats.
- final class dmr.throttling.Rate(*values)[source]¶
Throttling rates in seconds.
- second¶
1 second.
- minute¶
60 seconds.
- hour¶
60 * 60 seconds.
- day¶
24 * 60 * 60 seconds.
- class dmr.throttling.ThrottlingReport(controller: Controller[BaseSerializer])[source]¶
Get throttling data to be reported in response headers.
Unlike
dmr.exceptions.TooManyRequestsError, which reports the first stat for throttle that is failing, it reports all stats for all throttles.It will make N (which is the number of throttles) requests to cache.
Warning
This class does not handle exceptions at all. Because we don’t know how to report headers for failing cache connections. If you want to handle errors, catch them explicitly.
Added in version 0.7.0.
Backends¶
- class dmr.throttling.backends.CachedRateLimit[source]¶
Bases:
TypedDictRepresentation of a cached object’s metadata.
- class dmr.throttling.backends.BaseThrottleSyncBackend[source]¶
Base class for all throttling backends.
It must provide sync and async API for sync and async throttling classes.
- abstractmethod get(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle, *, cache_key: str) CachedRateLimit | None[source]¶
Sync get the state with no increments.
- abstractmethod incr(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle, *, cache_key: str, algorithm: BaseThrottleAlgorithm) CachedRateLimit[source]¶
Sync increment cached rate limit state.
Can be atomic, can be non atomic. Atomicity needs to be documented.
- class dmr.throttling.backends.BaseThrottleAsyncBackend[source]¶
Base class for all throttling backends.
It must provide sync and async API for sync and async throttling classes.
- abstractmethod async get(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: AsyncThrottle, *, cache_key: str) CachedRateLimit | None[source]¶
Sync get the state with no increments.
- abstractmethod async incr(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: AsyncThrottle, *, cache_key: str, algorithm: BaseThrottleAlgorithm) CachedRateLimit[source]¶
Async increment cached rate limit state.
- class dmr.throttling.backends.SyncDjangoCache(cache_name: str = 'default')[source]¶
Uses Django sync cache framework for storing the rate limiting state.
- get(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle, *, cache_key: str) CachedRateLimit | None[source]¶
Sync get the cached rate limit state.
- incr(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle, *, cache_key: str, algorithm: BaseThrottleAlgorithm) CachedRateLimit[source]¶
Sync increment cached rate limit state.
Can be atomic, can be non atomic. Atomicity needs to be documented.
- class dmr.throttling.backends.AsyncDjangoCache(cache_name: str = 'default')[source]¶
Uses Django async cache framework for storing the rate limiting state.
- async get(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: AsyncThrottle, *, cache_key: str) CachedRateLimit | None[source]¶
Async get the cached rate limit state.
- async incr(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: AsyncThrottle, *, cache_key: str, algorithm: BaseThrottleAlgorithm) CachedRateLimit[source]¶
Async increment cached rate limit state.
- final class dmr.throttling.backends.django_cache.UnsafeCacheBackendWarning[source]¶
Warning emitted when an unsafe cache backend is used for throttling.
- class dmr.throttling.backends.redis.SyncRedis(client: redis.Redis[Any])[source]¶
Uses sync Redis client for multiproccess safe rate-limiting.
This backend requires
redisclient library to be installed. We don’t ship it with thedjango-modern-rest.You would have to install it separately:
pip install redisSee also
- get(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle, *, cache_key: str) CachedRateLimit | None[source]¶
Sync get the cached rate limit state.
- incr(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle, *, cache_key: str, algorithm: BaseThrottleAlgorithm) CachedRateLimit[source]¶
Sync increment cached rate limit state.
Can be atomic, can be non atomic. Atomicity needs to be documented.
- initialize_algorithm(algorithm: BaseThrottleAlgorithm) None[source]¶
Initialize and prepare backend for the algorithm.
- class dmr.throttling.backends.redis.AsyncRedis(client: aioredis.Redis[Any])[source]¶
Uses async Redis client for multiproccess safe rate-limiting.
This backend requires
redisclient library to be installed. We don’t ship it with thedjango-modern-rest.You would have to install it separately:
pip install redisSee also
- async get(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: AsyncThrottle, *, cache_key: str) CachedRateLimit | None[source]¶
Async get the cached rate limit state.
- async incr(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: AsyncThrottle, *, cache_key: str, algorithm: BaseThrottleAlgorithm) CachedRateLimit[source]¶
Async increment cached rate limit state.
- initialize_algorithm(algorithm: BaseThrottleAlgorithm) None[source]¶
Initialize and prepare backend for the algorithm.
Algorithms¶
- class dmr.throttling.algorithms.BaseThrottleAlgorithm[source]¶
Base class for all throttling algorithms.
- abstractmethod access(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle | AsyncThrottle, cache_object: CachedRateLimit | None) CachedRateLimit[source]¶
Called when new access attempt is made.
- Returns:
Cached rate limiting state.
- Raises:
dmr.exceptions.TooManyRequestsError – when the limit is overused.
- abstractmethod report_usage(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle | AsyncThrottle, cache_object: CachedRateLimit | None) dict[str, str][source]¶
Reports the throttling usage, but does not additionally increment.
- class dmr.throttling.algorithms.SimpleRate[source]¶
Simple rate algorithm.
Defines a fixed window with a fixed amount of requests possible. When window is expired, resets the count of requests.
- access(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle | AsyncThrottle, cache_object: CachedRateLimit | None) CachedRateLimit[source]¶
Check access.
- report_usage(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle | AsyncThrottle, cache_object: CachedRateLimit | None) dict[str, str][source]¶
Reports the throttling usage, but does not additionally increment.
- class dmr.throttling.algorithms.LeakyBucket[source]¶
Leaky bucket algorithm.
Requests fill the bucket; tokens leak at a steady rate. Unlike
dmr.throttling.algorithms.SimpleRate, which resets after a fixed window,LeakyBucketdrains continuously providing smoother rate-limiting without allowing bursts at window boundaries.Internally, the bucket request level is stored in scaled units as
level * durationso all arithmetic stays integer-only. Each request addsdurationscaled units to the level. Every elapsed secondmax_requestsscaled units leak out.- access(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle | AsyncThrottle, cache_object: CachedRateLimit | None) CachedRateLimit[source]¶
Check access; raise when the bucket is full.
- report_usage(endpoint: Endpoint, controller: Controller[BaseSerializer], throttle: SyncThrottle | AsyncThrottle, cache_object: CachedRateLimit | None) dict[str, str][source]¶
Report throttling usage without incrementing.
Cache keys¶
- class dmr.throttling.cache_keys.BaseThrottleCacheKey(*, runs_before_auth: bool, name: str)[source]¶
Base class for all cache keys.
- abstractmethod __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) str | None[source]¶
Returns the cache key.
If string is returned, we use this as a cache key. If
Noneis returned, this request will be skipped from this exact throttling check. However, other keys may still be applied.
- class dmr.throttling.cache_keys.RemoteAddr(*, runs_before_auth: bool = True, name: str = 'RemoteAddr')[source]¶
Uses
REMOTE_ADDRfromrequest.METAas a cache key.Warning
Be sure to correctly configure your HTTP Proxy! Otherwise,
REMOTE_ADDRmight be set incorrectly.See also
See
django.http.HttpRequest.METAdocs.- __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) str | None[source]¶
Return
REMOTE_ADDRwhich is a user’s IP address, if it exists.
- class dmr.throttling.cache_keys.UserPk(*, runs_before_auth: Literal[False] = False, name: str = 'UserPk', exclude_superuser: bool = True, exclude_stuff: bool = True)[source]¶
Uses
request.user.pkas a cache key.Returns
Nonefor users that should be excluded from throttling checks or whenpkis not set.- __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) str | None[source]¶
Return
request.user.pkwhen user should be throttled.
- class dmr.throttling.cache_keys.JwtToken(*, runs_before_auth: Literal[False] = False, name: str = 'JwtToken')[source]¶
Uses a hash of JWT claims from
request.__dmr_jwt__as a cache key.Never use a full token string for cache key generation.
Prefer
jticlaim, fallback tosubclaim.Store only SHA-256 hash, never raw claim values.
Returns
Nonewhen jwt token is not set, or when bothjtiandsubare missing.- __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) str | None[source]¶
Return a hash of JWT
jti/subclaims as a cache key.
Headers¶
- class dmr.throttling.headers.BaseResponseHeadersProvider[source]¶
Base class for all header providers.
- abstractmethod provide_headers_specs() dict[str, HeaderSpec][source]¶
Provide a spec for headers for the OpenAPI.
- class dmr.throttling.headers.XRateLimit[source]¶
Provides
X-RateLimitheaders.There headers inside:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
It is based on popular convention and does not have a formal spec.
See also
- provide_headers_specs() dict[str, HeaderSpec][source]¶
Provides headers specification.
- class dmr.throttling.headers.RetryAfter[source]¶
Provides
Retry-Afterheader.It is based on the existing spec.
- provide_headers_specs() dict[str, HeaderSpec][source]¶
Provides headers specification.