Skip to content
UsageRail
Frameworks / Django

Django middleware

The Django middleware checks each request's API key, rate limit and quota before your view runs, and counts the request when the response is closed.

bash
pip install usagerail
settings.py
import os

MIDDLEWARE = [
    # ...
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "usagerail.django.UsageRailMiddleware",
]

USAGERAIL = {
    "app_key": os.environ["USAGERAIL_APP_KEY"],
    "auth": "api-key",
}
views.py
from django.http import JsonResponse


def generate(request):
    result = run_model(request.body)
    request.usagerail.track(result.tokens)  # optional: count tokens
    return JsonResponse(result.body)

The SDK needs Python 3.10 or newer and has no dependencies of its own. The key comes from the Authorization: Bearer header, or from x-api-key when the request has no bearer token.

Settings

Everything goes in the USAGERAIL dict: app_key, auth, consumer, on_reject, on_key_store_unavailable, stale_key_snapshot, on_quota_unavailable, snapshot, flush_on_exit and logger. They work as on the FastAPI page. consumer and on_reject can be functions or dotted paths, such as "myapp.usage.consumer".

Each process has one SDK instance per app key, however many times Django loads the middleware chain.

Count units instead of requests

In apps that count units, call request.usagerail.track(n) in the view with a whole number from 0 to 10,000,000. Route weights and reservations work as described under quotas.

Use your own authentication

List UsageRail after AuthenticationMiddleware, as in the example, and return the caller's ID:

myapp/usage.py
def consumer(request):
    if request.user.is_authenticated:
        return str(request.user.pk)
    return None
settings.py
USAGERAIL = {
    "app_key": os.environ["USAGERAIL_APP_KEY"],
    "auth": "custom",
    "consumer": "myapp.usage.consumer",
}

Returning None makes the request anonymous: it's recorded, but not rate limited, not counted against a quota and never billed.

Rejected requests

Refused requests get the statuses and JSON body listed on the Express page. To send your own response, point on_reject at a function that takes (rejection, request) and returns an HttpResponse; copy rejection.headers onto it so clients can still back off.

What gets counted

  • Each request is counted under the URL pattern that matched, such as /v1/items/<int:pk>, with its method and outcome. re_path patterns are recorded as written, without the leading ^.
  • 2xx and 3xx responses are billable; 4xx and 5xx responses and refused requests aren't.
  • A redirect added by APPEND_SLASH depends on the order of your middleware. With CommonMiddleware listed before UsageRail, as in Django's default settings, UsageRail sees the original 404: it's recorded under _unmatched and isn't billed. With UsageRail listed first, it sees the 301 and bills it as 1 unit, like any 3xx.
  • If an outer middleware replaces the response, the request is counted with the status UsageRail's middleware saw.
  • Django can't tell when a client disconnects early, so requests are always counted by their status.

Running in production

Counts are sent every 60 seconds and once more when the server shuts down. Under gunicorn that happens on its own: gunicorn exits normally on SIGTERM, and the last counts go out on the way out.

Under uvicorn or another ASGI server, wrap the application in asgi.py:

asgi.py
import os

from django.core.asgi import get_asgi_application

from usagerail.django import asgi

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
application = asgi(get_asgi_application())

Django doesn't handle the server's shutdown event, and uvicorn ends the process as soon as it has shut down, so without the wrapper every restart loses the counts that hadn't gone out yet. The wrapper answers that event: it sends the remaining counts, waiting up to 5 seconds, and then tells uvicorn it's done.

A process killed outright loses the counts it hadn't sent.