How API Rate Limiting Works with django-axes in PostHog
PostHog uses django-axes to protect authentication endpoints by tracking failed login attempts per IP address and username, automatically locking out users for 10 minutes after 30 failed attempts through middleware integration and explicit lockout checks.
The PostHog analytics platform implements brute-force protection for its REST API using django-axes, a Django package designed specifically to prevent automated authentication attacks. While general API rate limiting is handled by Django REST Framework (DRF) throttles, django-axes focuses exclusively on login-related endpoints such as /api/login/ and /api/webauthn/. Understanding this implementation reveals how production Django applications secure authentication flows through request tracking and credential monitoring.
Configuration Settings in posthog/settings/web.py
The django-axes integration is controlled through environment-specific settings that define failure thresholds and lockout durations. These settings reside in the web configuration file and determine how aggressively the system protects against brute-force attempts.
# posthog/settings/web.py
from datetime import timedelta
from posthog.utils import get_from_env, str_to_bool
AXES_ENABLED = get_from_env("AXES_ENABLED", not TEST, type_cast=str_to_bool)
AXES_FAILURE_LIMIT = get_from_env("AXES_FAILURE_LIMIT", 30, type_cast=int)
AXES_COOLOFF_TIME = timedelta(minutes=10)
AXES_LOCKOUT_CALLABLE = "posthog.api.authentication.axes_locked_out"
Key configuration parameters:
AXES_ENABLED– Toggles the entire protection system on or off via environment variablesAXES_FAILURE_LIMIT– Sets the threshold to 30 failed attempts before triggering a lockoutAXES_COOLOFF_TIME– Defines a 10-minute lockout duration using Python'stimedeltaAXES_LOCKOUT_CALLABLE– Points toposthog.api.authentication.axes_locked_out, ensuring all lockouts return a consistent JSON error response
Middleware and Backend Integration
Axes requires both a middleware component to capture incoming requests and an authentication backend to perform lockout checks. These are appended to Django's standard MIDDLEWARE and AUTHENTICATION_BACKENDS lists in the same configuration file.
# posthog/settings/web.py – middleware & backend registration
MIDDLEWARE.append("axes.middleware.AxesMiddleware")
AUTHENTICATION_BACKENDS.append("axes.backends.AxesBackend")
The AxesMiddleware intercepts every incoming request and extracts client IP addresses using AXES_IPWARE_META_PRECEDENCE_ORDER to accurately identify the source behind load balancers. The AxesBackend provides the is_locked helper method that checks whether a specific combination of IP address and username has exceeded the failure limit.
Login Flow Implementation in posthog/api/authentication.py
The authentication logic implements a double-check pattern using AxesProxyHandler to ensure lockouts are enforced immediately when the failure threshold is crossed. This occurs within the login serializer before credentials are validated.
# posthog/api/authentication.py
from axes.exceptions import AxesBackendPermissionDenied
from axes.handlers.proxy import AxesProxyHandler
handler = AxesProxyHandler
axes_credentials = {"username": validated_data["email"]}
# Pre-authentication lockout check
if handler.is_locked(axes_request, credentials=axes_credentials):
raise AxesBackendPermissionDenied("Account locked: too many login attempts.")
The serializer performs the first check before verifying passwords. If authentication fails, the code performs a second check immediately after to detect whether this latest failure triggered the lockout threshold. This ensures the user receives a lockout response on the exact request that crosses the limit, not on the subsequent request.
Similar implementations exist in posthog/api/webauthn.py for WebAuthn authentication flows, maintaining consistent protection across all authentication methods.
Lockout Response Format
When a lockout is triggered, django-axes invokes the callable defined in AXES_LOCKOUT_CALLABLE. The axes_locked_out function in posthog/api/authentication.py constructs a standardized JSON error payload that client libraries can parse consistently.
# posthog/api/authentication.py – lockout response builder
def axes_locked_out(*args, **kwargs):
return JsonResponse(
{
"type": "authentication_error",
"code": "too_many_failed_attempts",
"detail": f"Too many failed login attempts. Please try again in {int(settings.AXES_COOLOFF_TIME.seconds / 60)} minutes.",
"attr": None,
},
status=status.HTTP_403_FORBIDDEN,
)
The response returns HTTP 403 Forbidden (maintained for legacy compatibility) with a JSON body containing the remaining lockout duration calculated from AXES_COOLOFF_TIME. The LoginViewSet catches AxesBackendPermissionDenied exceptions and delegates to this callable to ensure uniform error formatting.
DRF Throttling vs django-axes
PostHog implements two distinct rate-limiting layers:
- django-axes protects authentication endpoints specifically, tracking only failed login attempts
- DRF throttles (
BurstRateThrottleandSustainedRateThrottle) apply to all API requests whenRATE_LIMIT_ENABLEDis true
These systems operate independently. While axes blocks brute-force attacks against passwords, DRF throttles prevent general API abuse across all endpoints.
Summary
- django-axes is configured in
posthog/settings/web.pywith a 30-attempt limit and 10-minute cooldown period, toggled via theAXES_ENABLEDenvironment variable - Middleware integration through
AxesMiddlewareandAxesBackendcaptures request metadata and IP addresses for every authentication attempt - Double-validation pattern in
posthog/api/authentication.pyusesAxesProxyHandler.is_locked()to check lockout status both before and after credential verification - Uniform error responses are generated by the
axes_locked_outcallable, returning structured JSON with 403 status codes for locked accounts - Separation of concerns exists between django-axes (authentication protection) and DRF throttles (general API rate limiting)
Frequently Asked Questions
How many failed attempts trigger a lockout in PostHog?
By default, 30 consecutive failed attempts trigger a django-axes lockout. This threshold is controlled by the AXES_FAILURE_LIMIT setting in posthog/settings/web.py, which can be overridden via the AXES_FAILURE_LIMIT environment variable for different deployment environments.
How does django-axes identify clients for rate limiting?
The system uses IP address extraction via AXES_IPWARE_META_PRECEDENCE_ORDER in combination with the username credential. The AxesMiddleware processes incoming requests to determine the actual client IP behind proxy layers, ensuring accurate tracking per source address and account combination.
What is the difference between django-axes and DRF throttling?
django-axes specifically monitors and limits failed authentication attempts on login endpoints, while DRF throttles (BurstRateThrottle and SustainedRateThrottle) limit the total volume of all API requests across any endpoint. Axes protects against password brute-forcing, whereas DRF throttles prevent general API abuse and resource exhaustion.
Can the lockout duration be customized?
Yes, the cooldown period defaults to 10 minutes via AXES_COOLOFF_TIME in posthog/settings/web.py, but you can modify this by setting the environment variable to a different timedelta string or by directly adjusting the setting in your deployment configuration. The axes_locked_out function automatically calculates the remaining minutes from this setting for user-facing error messages.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →