Django Quickstart
Protect a Django view with Baldur. No Redis, no Docker, no environment variables. The in-memory fallback covers the whole first run.
Supports Python 3.11–3.13 and Django 4.2 / 5.2 LTS / 6.x. Assumes you have built a Django app before.
1. Install
pip install baldur-framework[django]
2. Add Baldur to your settings
Add baldur.adapters.django to INSTALLED_APPS. Its app config calls
baldur.init() on startup for you — there is nothing else to wire.
"""Minimal Django settings for the Baldur quickstart.
Zero infrastructure: in-memory SQLite + Baldur's in-memory fallback (no Redis,
no env vars). Adding ``baldur.adapters.django`` to ``INSTALLED_APPS`` calls
``baldur.init()`` automatically on startup via the app's ``ready()`` hook.
Production: see the "Add Redis for production" appendix in
``docs/getting-started/django.md`` — the in-memory fallback is single-process
only and is NOT safe for multi-worker deployments.
"""
from __future__ import annotations
SECRET_KEY = "quickstart-insecure-key-do-not-use-in-production" # noqa: S105
DEBUG = True
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS = [
"django.contrib.contenttypes",
"django.contrib.auth",
# Baldur Django integration — calls baldur.init() on startup.
"baldur.adapters.django",
]
ROOT_URLCONF = "urls"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}
USE_TZ = True
That also wires HTTP latency (RED) automatically: the adapter injects its
metrics middleware on startup, so the baldur_http_request_duration_seconds
histogram behind the overview's HTTP Latency panel populates with no middleware
to add — the same out-of-the-box behavior as the Flask and FastAPI quickstarts.
3. Protect a view
@baldur.protected("demo", dlq=True) wraps the view in Baldur's composed
resilience pipeline (circuit breaker on by default):
"""Minimal Django view protected by Baldur's marquee facade.
``@baldur.protected("demo", dlq=True)`` wraps the view in Baldur's composed
resilience pipeline (circuit breaker on by default; ``dlq=True`` opts the view
into dead-letter capture of final failures). With zero configuration it uses
the in-memory fallback — no Redis, no env vars. See
``docs/getting-started/django.md`` for the 5-minute walkthrough.
"""
from __future__ import annotations
from django.http import HttpRequest, JsonResponse
import baldur
@baldur.protected("demo", dlq=True)
def demo(request: HttpRequest) -> JsonResponse:
"""Return a JSON payload through Baldur's resilience pipeline."""
return JsonResponse({"status": "ok", "service": "demo"})
dlq=True opts the view into the dead-letter queue:
a final failure is captured with a snapshot of the call's arguments so it can be
replayed once the dependency recovers. Capture stores your request data, which
is why it is opt-in per call
rather than on by default.
Route it:
"""URL configuration for the Baldur Django quickstart."""
from __future__ import annotations
from django.urls import path
from views import demo
urlpatterns = [
path("demo/", demo),
]
4. Run it
Start the dev server and call the route:
python manage.py runserver
curl http://127.0.0.1:8000/demo/
# {"status": "ok", "service": "demo"}
That's it. The response just travelled through a circuit breaker.
The unapplied-migrations notice at startup is expected here
Baldur ships Django models for its own admin API, and this settings file
keeps the database at :memory:, so every start begins with an empty one
and Django says so. The demo route never touches the database, so the
notice is harmless — and manage.py migrate will not silence it, because
the next process gets a fresh in-memory database again. Point DATABASES
at a file or a real server and the notice goes away once you migrate.
Open the console
baldur.init() also started Baldur's admin server on loopback, so the built-in
Web Console is already serving — no
extra step, and nothing to configure:
open http://127.0.0.1:9090/ # any browser; the page is served on loopback only
If another process already holds that port, Baldur does not take it over: it
logs admin.autostart_failed and your app keeps serving without the console.
See Baldur's events
Baldur logs to stdout when your app has no root handler; when it has one
(logging.basicConfig, a root entry in LOGGING), Baldur's events go
through your handlers in your format. Set BALDUR_LOG_LEVEL=INFO to watch
circuit breaker and rate-limit events as you exercise the endpoint:
export BALDUR_LOG_LEVEL=INFO # circuit opened/closed, rate-limit blocks, ...
Verify without a browser
The quickstart ships a smoke test that drives the view through Django's in-process test client — no server, no infra:
pytest examples/quickstart_django/test_smoke.py
Browse the full runnable app:
examples/quickstart_django/.
Going to production
The in-memory fallback is single-process only
The zero-config path uses Baldur's in-memory cache. It keeps state in a
per-process store, so copying this quickstart into a multi-worker
deployment (gunicorn --workers N, uvicorn --workers N) does not
degrade gracefully: idempotency keys, rate-limit counters, and circuit
breaker state diverge silently per worker. That breaks correctness,
not just scale. The in-memory store also grows unbounded. This is a
hazard, not a tuning knob: give Baldur a shared backend before you run
more than one worker.
Point Baldur at Redis so all workers share state. No code changes needed: set one environment variable before starting the server:
pip install baldur-framework[django,redis]
export BALDUR_REDIS_URL=redis://localhost:6379/0
export BALDUR_ENVIRONMENT=production
Those two variables are the only addition the production path needs over the
quickstart path. Declaring the environment is what turns the hazard above into a
rule Baldur enforces: with BALDUR_ENVIRONMENT=production set and
BALDUR_REDIS_URL missing, baldur.init() refuses to start rather than let a
shared guarantee degrade to per-worker memory. For a deliberate single-process
deployment on in-memory state, BALDUR_TEST_MODE=true opts out of that check,
and of Baldur's other production configuration checks with it.