Rate limiting is a critical security feature that protects your application from abuse, brute force attacks, and resource exhaustion. django-allauth includes comprehensive rate limiting that’s enabled by default and requires no external dependencies beyond Django’s cache framework.
Rate limiting requires a proper cache backend. It will not work correctly with Django’s DummyCache. Use Redis, Memcached, or database caching in production.
"5/m/ip" # 5 requests per minute per IP"20/h/user" # 20 requests per hour per user "3/30s/key" # 3 requests per 30 seconds per key"10/5m/ip,5/m/key" # Multiple limits (both must pass)
Modify individual rate limits while keeping defaults:
# settings.pyACCOUNT_RATE_LIMITS = { # More restrictive login limit "login": "10/m/ip", # Allow more signups per IP (e.g., public WiFi) "signup": "50/m/ip", # Stricter password reset "reset_password": "10/m/ip,3/m/key", # All other limits use defaults}
Disabling rate limits significantly increases your application’s attack surface. Only do this in controlled environments or with alternative protection mechanisms.
Default:10/m/ip,5/5m/keyCritical for security - Prevents brute force password attacks.
ACCOUNT_RATE_LIMITS = { # 10 per minute per IP, 5 per 5 minutes per username/email "login_failed": "10/m/ip,5/5m/key",}
When exceeded, users are temporarily locked out even with correct credentials.
Important: This only protects allauth’s login view. It does not protect Django’s admin login. See the admin protection docs for securing admin login.
From source (rate_limits.rst:43-47):
Restricts the allowed number of failed login attempts. When exceeded, the user is prohibited from logging in for the remainder of the rate limit. Important: while this protects the allauth login view, it does not protect Django’s admin login from being brute forced.
signup
Default:20/m/ipUser registration attempts.
ACCOUNT_RATE_LIMITS = { "signup": "30/m/ip",}
Prevents automated account creation and spam registrations.
Critical Security Consideration: Accurate IP detection is essential for rate limiting to work correctly.django-allauth cannot reliably determine the client IP address out of the box because the correct method depends on your infrastructure (load balancers, proxies, CDNs).The X-Forwarded-For header can be trivially spoofed, allowing attackers to bypass rate limits entirely.
Configure how many proxies are under your control:
# settings.py# Number of trusted proxiesALLAUTH_TRUSTED_PROXY_COUNT = 1 # e.g., one load balancer# Custom IP header from trusted proxyALLAUTH_TRUSTED_CLIENT_IP_HEADER = None # Default
How it works:With ALLAUTH_TRUSTED_PROXY_COUNT = 1:
X-Forwarded-For: client, proxy1, proxy2
Takes the IP from position: count from the right
Result: proxy1 (second from right)
From source (rate_limits.rst:36-45):
As the X-Forwarded-For header can be spoofed, you need to configure the number of proxies that are under your control and hence, can be trusted. The default is 0, meaning, no proxies are trusted. As a result, the X-Forwarded-For header will be disregarded by default.
from allauth.account.adapter import DefaultAccountAdapterclass MyAccountAdapter(DefaultAccountAdapter): def get_client_ip(self, request): """ Custom IP extraction logic. Example: App behind Cloudflare and then AWS ALB. """ # Trust Cloudflare's CF-Connecting-IP cf_ip = request.META.get('HTTP_CF_CONNECTING_IP') if cf_ip: return cf_ip # Fallback to X-Forwarded-For with 2 trusted proxies x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: # Get second IP from the right (after 2 proxies) ips = [ip.strip() for ip in x_forwarded_for.split(',')] if len(ips) >= 2: return ips[-2] # Ultimate fallback return request.META.get('REMOTE_ADDR')# settings.pyACCOUNT_ADAPTER = 'myapp.adapters.MyAccountAdapter'
"""Rate limiting in this implementation relies on a cache and uses non-atomicoperations, making it vulnerable to race conditions. As a result, users mayoccasionally bypass the intended rate limit due to concurrent access. However,such race conditions are rare in practice. For example, if the limit is set to10 requests per minute and a large number of parallel processes attempt to testthat limit, you may occasionally observe slight overruns—such as 11 or 12requests slipping through. Nevertheless, exceeding the limit by a large marginis highly unlikely due to the low probability of many processes entering thecritical non-atomic code section simultaneously."""
Rate limiting uses cache-based tracking with non-atomic operations. This means:
✅ Performant and scalable
✅ No database overhead
⚠️ Slight overruns possible under high concurrency (acceptable trade-off)
def _consume_single_rate( request, *, action: str, rate: Rate, key=None, user=None, dry_run: bool = False, raise_exception: bool = False,) -> Optional[SingleRateLimitUsage]: """ Consume one rate limit slot. Returns usage object if allowed, None if rate limited. """ cache_key = get_cache_key(request, action=action, rate=rate, key=key, user=user) history = cache.get(cache_key, []) now = time.time() # Remove expired entries while history and history[-1] <= now - rate.duration: history.pop() allowed = len(history) < rate.amount if allowed and not dry_run: history.insert(0, now) cache.set(cache_key, history, rate.duration) if not allowed and raise_exception: raise RateLimited return usage if allowed else None
History tracking:
# Cache stores list of timestampscache_key = "allauth:rl:login:ip:192.168.1.1"cache_value = [ 1709823456.123, # Most recent 1709823450.456, 1709823445.789, # ... up to rate.amount]
from allauth.core import ratelimit# Clear rate limit for specific userratelimit.clear( request, action="login_failed", key="[email protected]")# Clear for user objectratelimit.clear( request, action="change_password", user=request.user)
from allauth.account.adapter import DefaultAccountAdapterclass MetricsAccountAdapter(DefaultAccountAdapter): def is_ajax(self, request): """Override to track rate limit hits.""" from allauth.core import ratelimit # Before any action, check if rate limited # (This is simplified - actual implementation varies) return super().is_ajax(request)
# Integrate with your monitoring systemif rate_limit_exceeded: metrics.increment('rate_limit.exceeded', tags=['action:login']) if hits > threshold: alert_security_team(ip_address)
Secure IP Detection
Always configure IP detection for your infrastructure: