# How API Rate Limiting Works with django-axes in PostHog

> Learn how PostHog secures authentication with django-axes API rate limiting. Discover automatic lockout after 30 failed attempts per IP or username to prevent brute-force attacks.

- Repository: [PostHog/posthog](https://github.com/PostHog/posthog)
- Tags: how-to-guide
- Published: 2026-04-25

---

**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`](https://github.com/PostHog/posthog/blob/main/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.

```python

# 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 variables
- **`AXES_FAILURE_LIMIT`** – Sets the threshold to **30 failed attempts** before triggering a lockout
- **`AXES_COOLOFF_TIME`** – Defines a **10-minute** lockout duration using Python's `timedelta`
- **`AXES_LOCKOUT_CALLABLE`** – Points to `posthog.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.

```python

# 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`](https://github.com/PostHog/posthog/blob/main/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.

```python

# 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`](https://github.com/PostHog/posthog/blob/main/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`](https://github.com/PostHog/posthog/blob/main/posthog/api/authentication.py) constructs a standardized JSON error payload that client libraries can parse consistently.

```python

# 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** (`BurstRateThrottle` and `SustainedRateThrottle`) apply to all API requests when `RATE_LIMIT_ENABLED` is 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.py`](https://github.com/PostHog/posthog/blob/main/posthog/settings/web.py)** with a 30-attempt limit and 10-minute cooldown period, toggled via the `AXES_ENABLED` environment variable
- **Middleware integration** through `AxesMiddleware` and `AxesBackend` captures request metadata and IP addresses for every authentication attempt
- **Double-validation pattern** in [`posthog/api/authentication.py`](https://github.com/PostHog/posthog/blob/main/posthog/api/authentication.py) uses `AxesProxyHandler.is_locked()` to check lockout status both before and after credential verification
- **Uniform error responses** are generated by the `axes_locked_out` callable, 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`](https://github.com/PostHog/posthog/blob/main/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`](https://github.com/PostHog/posthog/blob/main/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.