Skip to content

Dynamic Rate-Limit Rules

Dynamic rules let you define and change rate limits at runtime, from the Django admin or the ORM, without editing settings or redeploying. Rules are stored in the database as RateLimitRule rows. When the rate-limit middleware handles a request, it matches the request against the active rules and, if one matches, uses that rule's rate, key, and block instead of the static RATELIMIT_MIDDLEWARE configuration.

This is a middleware feature: it applies to requests flowing through RateLimitMiddleware. It does not change the behavior of the @rate_limit decorator.

Setup

1. Add the app and run migrations

RateLimitRule is a Django model, so the app must be installed and its migrations applied.

# settings.py
INSTALLED_APPS = [
    # ...
    "django_smart_ratelimit",
]

MIDDLEWARE = [
    # ...
    "django_smart_ratelimit.middleware.RateLimitMiddleware",
]
python manage.py migrate

2. Enable dynamic rules

Dynamic rules are off by default. Turn them on with a single setting:

# settings.py
RATELIMIT_USE_DYNAMIC_RULES = True

# Optional: how long (seconds) the active rule set is cached. Default 60.
RATELIMIT_RULE_CACHE_TIMEOUT = 60

# The middleware still needs its normal config; matching rules override it.
RATELIMIT_MIDDLEWARE = {
    "BACKEND": "redis",
    "DEFAULT_RATE": "1000/m",
}

With RATELIMIT_USE_DYNAMIC_RULES = False (the default), the middleware never queries the rule table and behaves exactly as before.

Defining a rule

You can create rules in the Django admin (the RateLimitRule admin is registered automatically when the app is installed) or directly via the ORM.

from django_smart_ratelimit.models import RateLimitRule

RateLimitRule.objects.create(
    name="api-strict",
    path_pattern=r"^/api/",   # regex matched against request.path
    method="ALL",             # or "GET,POST"
    rate="2/m",               # 2 requests per minute
    key="ip",                 # ip | user | header:X-API-Key
    algorithm="fixed_window",
    block=True,
    priority=10,
    is_active=True,
)

Fields

Field Type Default Description
name str required, unique Identifier for the rule. Also used in the rate-limit key (rule:<name>:...) and in recorded events.
description str "" Free-text note for operators.
path_pattern str required A regular expression matched against request.path with re.search, e.g. ^/api/. Validated at save time.
method str "ALL" ALL, or a comma-separated list of HTTP methods such as GET,POST. Case-insensitive.
rate str required Rate string such as 100/m, 1000/h, or 10/30s. Validated at save time.
key str "ip" Per-client key: ip, user (falls back to IP for anonymous users), or header:<Header-Name> such as header:X-API-Key.
algorithm str "fixed_window" One of fixed_window, sliding_window, token_bucket, leaky_bucket.
block bool True When True, requests over the limit get a 429. When False, the limit is counted but not enforced.
is_active bool True Only active rules are loaded and matched. Inactive rules are ignored.
priority int 0 Higher priority wins when several rules match the same request.

Both the admin save and RateLimitRule.save() run full_clean(), so an invalid rate string or an invalid path_pattern regex raises a ValidationError before the row is written.

How matching and priority work

For each request, the middleware asks the rule engine for the single highest-priority active rule that matches. A rule matches when:

  1. re.search(rule.path_pattern, request.path) finds a match, and
  2. the request's HTTP method is in rule.method (or the rule's method is ALL).

Active rules are ordered by -priority then name, so when several rules match the same request, the one with the highest priority is applied. If no rule matches, the request falls back to the static middleware configuration.

RateLimitRule.objects.create(name="lo", path_pattern="^/api/", rate="9/m", priority=1)
RateLimitRule.objects.create(name="hi", path_pattern="^/api/", rate="1/m", priority=9)
# A GET /api/x request matches both; "hi" (priority 9) is applied -> 1/m.

Overriding static config

When dynamic rules are enabled and a rule matches, that rule fully determines the limit for the request: its rate, its block flag, and a key derived from its key field. This overrides whatever the static RATE_LIMITS / DEFAULT_RATE settings would have produced.

# settings.py
RATELIMIT_USE_DYNAMIC_RULES = True
RATELIMIT_MIDDLEWARE = {"BACKEND": "memory", "DEFAULT_RATE": "1000/m"}
# A rule scoped to /api/ at 2/m.
RateLimitRule.objects.create(
    name="api-strict", path_pattern=r"^/api/", rate="2/m", key="ip", priority=10
)

With the rule above, requests to /api/... are limited to 2/m (the rule wins), while requests to any other path still use the static DEFAULT_RATE of 1000/m (no rule matches, so the fallback applies).

Each rule gets its own counter namespace: the key the middleware uses is rule:<name>:<client>, where <client> is the IP, user:<pk>, or the header value, according to the rule's key. So two rules with overlapping path patterns count independently.

Caching and invalidation

Loading every active rule from the database on every request would be expensive, so the rule engine caches the active rule set in process memory. The cache TTL is RATELIMIT_RULE_CACHE_TIMEOUT seconds (default 60).

Edits made through the ORM or the admin take effect immediately: the app wires Django post_save and post_delete signals on RateLimitRule (from AppConfig.ready()) so that saving or deleting a rule invalidates the cache. The TTL is the upper bound on staleness for changes that bypass those signals (for example a bulk UPDATE run directly against the database, or an edit made in a different process/worker).

# Editing a rule at runtime takes effect on the next request in this process.
rule = RateLimitRule.objects.get(name="api-strict")
rule.rate = "10/m"
rule.save()   # post_save signal invalidates the cache

If you need to drop the cache by hand:

from django_smart_ratelimit.rules import rule_engine

rule_engine.invalidate_cache()

Reloading rules manually

After a change that bypasses the model signals (a bulk DB import, a raw SQL update, or an edit applied in another worker), invalidate the cache in this process with the management command:

python manage.py ratelimit_reload_rules

It invalidates the rule cache so the next request reloads from the database, and reports how many active rules there are:

Rate-limit rule cache reloaded (3 active rule(s)).

Note that, like the in-process cache itself, this affects only the process it runs in. With multiple workers, rely on the per-process signal invalidation and the RATELIMIT_RULE_CACHE_TIMEOUT TTL for changes to propagate everywhere, or run the command (or restart) per worker.

Managing rules in the admin

The RateLimitRule admin is registered automatically. It lists name, path_pattern, method, rate, key, algorithm, is_active, and priority, with is_active and priority editable inline. It also provides two bulk actions, Enable selected rules and Disable selected rules, which save each rule individually so the cache is invalidated.

The admin also exposes a read-only Rate Limit Counter view (RateLimitCounter) for inspecting live fixed-window counters; these rows are created by the limiter and cannot be added or edited by hand.