Rate limiting
Application-layer per-IP throttling inside BaseResource.dispatch, with separate buckets for general API traffic, login attempts and abuse detection.
What this page covers
0-mcp has two protection layers:
- SecurityMiddleware — edge blocking for scanner paths, bad UAs and repeated 4xx probing before any view runs.
- Rate limiting in dispatch — request throttling for
BaseResourceendpoints (api,login,abusebuckets).
This page describes the second layer: the rate limiting that runs inside dispatch.
Default limits
# 0-mcp defaults — override inside the MCP bag in settings.py
MCP = {
'RATE_LIMITS': {
'api': [
{'interval': 1000, 'limit': 4}, # 4 requests/sec
{'interval': 5000, 'limit': 20}, # 20 requests/5s
],
'login': [
{'interval': 5000, 'limit': 3}, # 3 login attempts/5s
{'interval': 3600000, 'limit': 50}, # 50 login attempts/h
],
'abuse': [
{'interval': 5000, 'limit': 20},
{'interval': 3600000, 'limit': 200},
],
},
}How it works
- Every
BaseResourcerequest increments counters keyed by IP and interval in Redis. - If any
apibucket is over limit → 429 "Slow down". - If
abusebucket fires → IP is blocked for 24 hours (403). /loginpaths use theloginbucket instead ofapi.
Credential-path auto-defense
On top of the api/login buckets, paths that handle credentials get a tighter per-IP bucket out of the box. Match is segment-based — a configured /forgot matches /forgot, /login/forgot, /user/forgot/..., but not /forgotten.
# Framework defaults (active out of the box; override in MCP to change)
MCP = {
'CREDENTIAL_PATHS': [
'/forgot', '/change_password', '/signup',
'/reset', '/recover', '/register',
],
'CREDENTIAL_RATE_LIMIT': {'limit': 5, 'window': 30}, # 5 hits / 30s per IP
}Setting CREDENTIAL_PATHS in your project replaces the framework default — there is no merge. To add project-specific paths (e.g. /activate, /email_validate), copy the default list and extend it. Set to [] to disable entirely.
Per-resource declarative rate-limits
For finer control — per-user, per-email, lockout patterns — declare a rate_limits array on the resource. Each entry is its own bucket; the first to exceed raises 429. Buckets run after authentication, so callable keys can read self.user.
class ChangePasswordResource(BaseResource):
rate_limits = [
# short — burst protection per user (correct under shared NAT)
{'name': 'short', 'key': lambda r: r.user['id'],
'limit': 5, 'window': 60},
# long — lockout (50 attempts in 24h = blocked 24h)
{'name': 'lockout', 'key': lambda r: r.user['id'],
'limit': 50, 'window': 86400},
]Keys returning None are silently skipped — useful for buckets that should only fire for authenticated traffic. Combine arbitrary keys: lambda r: r.body.get('email') for per-email buckets on a /forgot endpoint defends against IP-rotating attackers targeting one specific account.
Shared block list
Both layers converge on the same blocked-IP store: rate_limit:blocked:<ip> with TTL 24h. SecurityMiddleware can place an IP there before any view runs; BaseResource.check_is_blocked consults the same key inside dispatch.
To unblock manually:
redis-cli DEL rate_limit:blocked:1.2.3.4
Inside dispatch
Rate limit runs first — before authentication, before the handler. A blocked IP cannot even probe whether a session is valid.
Rate limit keys are scoped by IP. TRUSTED_PROXIES makes sure the IP is the real client, not the load balancer.
Non-BaseResource views only get the middleware layer. They do not automatically inherit the RATE_LIMITS['api'/'login'/'abuse'] dispatch buckets.
0-mcp by Stamatios Stamou Jr — github.com/ssjunior/0-mcp