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:

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

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