Django
Requires Django 5.2+.
For the new-user onramp see the Django quickstart. This page is the reference.
Install
Section titled “Install”Pick the engine you use, install with the matching extra. Each extra pulls the engine adapter AND its companion scheduler, so one command covers both the worker and beat processes.
pip install z4j-django[celery] # Celery + celery-beatpip install z4j-django[rq] # RQ + rq-schedulerpip install z4j-django[dramatiq] # Dramatiq + APSchedulerpip install z4j-django[huey] # Huey + huey-periodicpip install z4j-django[arq] # arq + arq-cronpip install z4j-django[taskiq] # TaskIQ + taskiq-schedulerpip install z4j-django[all] # every engine (CI / kitchen sink)pip install z4j-django with no extra installs only the framework adapter. Useful when engine packages are managed elsewhere; otherwise pick an engine extra.
Add to INSTALLED_APPS:
INSTALLED_APPS = [ ..., "django_celery_beat", # if applicable "z4j_django",]The order doesn’t matter for z4j (z4j-django auto-imports z4j-celery on module load so the celery worker_init signal handler is always wired) but django_celery_beat should come before any app whose migrations depend on it.
Settings
Section titled “Settings”All settings live under the Z4J dict. Env-var overrides take priority over the dict.
Z4J = { "brain_url": env("Z4J_BRAIN_URL"), # required "token": env("Z4J_TOKEN"), # required "hmac_secret": env("Z4J_HMAC_SECRET"), # required "project_id": env("Z4J_PROJECT_ID"), # required "agent_name": env("Z4J_AGENT_NAME", default=None), # optional}Required
Section titled “Required”| Key | Env var | Meaning |
|---|---|---|
brain_url | Z4J_BRAIN_URL | Base URL of the brain (http://localhost:7700 for local dev; https://... in prod). The agent appends /ws/agent automatically and converts http(s):// → ws(s)://. |
token | Z4J_TOKEN | Plaintext bearer token from the dashboard agent-mint dialog. NOT the first-boot setup token. |
hmac_secret | Z4J_HMAC_SECRET | Per-project HMAC secret returned alongside the token in the same mint dialog. The agent refuses to start without it. |
project_id | Z4J_PROJECT_ID | Project slug. The brain takes the authoritative project from the token’s record, so any non-empty value works locally; mismatch doesn’t fail auth. |
Optional
Section titled “Optional”| Key | Env var | Default | Meaning |
|---|---|---|---|
agent_name | Z4J_AGENT_NAME | unset | Human label sent in the hello frame’s host.name field. Useful for distinguishing multiple workers sharing one token. |
environment | Z4J_ENVIRONMENT | "production" | Free-form label attached to every event. |
tags | (dict only) | {} | Per-deployment tags echoed on every event. |
dev_mode | Z4J_DEV_MODE | False | Disables the plain-ws:// rejection for non-loopback hosts. Loopback hosts (localhost/127.0.0.1/::1) are auto-allowed without this flag. Only set this for trusted internal networks where TLS termination is somewhere upstream. |
strict_mode | Z4J_STRICT_MODE | False | Crash on config error instead of degrading gracefully. |
autostart | Z4J_AUTOSTART | True | Set False to construct the runtime without starting it (test rigs). |
buffer_path | Z4J_BUFFER_PATH | ~/.z4j/buffer-{pid}.sqlite | Where the on-disk event buffer lives. If ~/.z4j is not writable (gunicorn under www-data, systemd DynamicUser=yes, etc.) the agent automatically falls back to $TMPDIR/z4j-{uid}/buffer-{pid}.sqlite (mode 0700) and logs a single WARNING. Both roots are inside the security clamp; explicit overrides outside them are rejected. See service-user deployments. |
Celery app override (rare)
Section titled “Celery app override (rare)”The Celery app is auto-detected via 5 candidates (see quickstart §Auto-detect). If your layout is unusual, set:
CELERY_APP = "myproject.celery:app"That’s a top-level Django setting, not part of the Z4J dict.
Lifecycle
Section titled “Lifecycle”Web process (runserver / gunicorn / uvicorn)
Section titled “Web process (runserver / gunicorn / uvicorn)”Z4JDjangoConfig.ready() runs once per worker. It:
- Skips if
Z4J_DISABLED=true. - Skips if running a one-shot management command (
migrate,collectstatic,check,shell,test, etc.). - Skips in the Django autoreload parent process under
runserver(only the child withRUN_MAIN=trueopens a WebSocket - prevents the brain from force-closing duplicate connections with code 4002). - Skips when launched as a Celery sub-command (
celery worker,celery beat). The Celery worker process gets its agent fromz4j_celery.worker_bootstrapinstead, with the engine attached. - Otherwise: builds the runtime, registers with the process-wide singleton, and starts the WebSocket.
Celery worker process
Section titled “Celery worker process”Triggered by celery.signals.worker_init. Imports the Celery engine adapter, calls install_agent(engines=[engine]). The engine attaches to celery’s signals (task_prerun, task_postrun, task_retry, etc.).
z4j-django eagerly imports z4j_celery at module-load time so the worker_init signal handler is always wired, even though z4j-django itself doesn’t start the agent under celery.
Shutdown
Section titled “Shutdown”atexit hook flushes the buffer and closes the WebSocket.
In tests, set Z4J_DISABLED=1 in your test runner env to skip startup entirely.
Verify with doctor
Section titled “Verify with doctor”python manage.py z4j_doctor runs the same probes the agent runtime does (buffer dir writable, brain DNS / TCP / TLS, WebSocket upgrade) but synchronously and without starting the persistent agent. Use it to diagnose silent startup failures.
# Always run as the same user the service runs under.sudo -u www-data /srv/picker/venv/bin/python manage.py z4j_doctor
# Skip the WS round-trip when the brain is intentionally offline:python manage.py z4j_doctor --no-websocket
# Machine-readable for scripting:python manage.py z4j_doctor --jsonOutput:
z4j-doctor (django)=================== brain_url: https://tasks.example.com/ project_id: picker buffer_path: /tmp/z4j-33/buffer-7281.sqlite transport: auto
[OK] buffer_path OK: buffer dir /tmp/z4j-33 is writable [OK] dns OK: tasks.example.com -> 198.51.100.42 [OK] tcp OK: TCP connect to tasks.example.com:443 [OK] tls OK: TLS TLSv1.3 to tasks.example.com (cert CN='tasks.example.com') [OK] websocket OK: ws upgrade to https://tasks.example.com/ succeeded
engines auto-detected: celeryExits 0 if every probe passes, 1 otherwise. Catches the gunicorn-under-www-data startup failure, NAT / firewall / cert mismatches, and wrong-token / wrong-project_id authentication problems with a specific failure reason. See service-user deployments.
Multi-process / Gunicorn
Section titled “Multi-process / Gunicorn”Each gunicorn worker is its own agent (one WebSocket per worker). The brain’s (project_id, agent_name) unique constraint means all workers must share one agent token OR each must have a distinct agent_name.
The default behavior - one worker = one WS connection - shows per-worker load in the dashboard. To coalesce at the display level, set Z4J_AGENT_NAME to the same value across workers, but you’ll lose per-process visibility.
Channels / ASGI
Section titled “Channels / ASGI”z4j-django auto-detects ASGI (Daphne / Uvicorn / Hypercorn) and integrates with the existing event loop - no extra configuration.
Troubleshooting
Section titled “Troubleshooting”First, run python manage.py z4j_doctor — it surfaces the most common failures with a specific reason. The list below covers the ones the doctor can’t diagnose on its own.
- Agent never connects - check
DJANGO_SETTINGS_MODULEis set,INSTALLED_APPSincludesz4j_django, and check startup logs for the[z4j] agent startingline. If the service runs underwww-dataand the log showsPermissionError: ... /var/www/.z4j, upgrade to z4j-bare 1.0.6+ which auto-relocates the buffer to$TMPDIR/z4j-{uid}(see service-user deployments). hmac_secret is required- you used the first-boot setup token instead of an agent token. See quickstart troubleshooting.refusing plain ws:// connection- non-loopback host without TLS. Usehttps://or setZ4J_DEV_MODE=truefor trusted internal networks.connection closed during send: received 4002- duplicate connection from the same agent token. The brain accepts only one WebSocket per token; the older one is force-closed. Mint a second agent for the second process.- Schedule CRUD disabled - requires
django-celery-beatwithDatabaseScheduler. Filesystem schedulers are read-only.
See engines and the agent runtime reference for more.