Django
Requires Django 5.2.17 or newer (below Django 6) on Python 3.11, or Django 6.0.8 or newer with no adapter cap on Python 3.12+. A standalone install can resolve Django 6.1; Celery/all extras currently resolve 6.0.8 because django-celery-beat declares Django<6.1. See the compatibility matrix for the full marker string.
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_ready signal handler is 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 z4j (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. z4j 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" |
Reserved deployment label available to adapters; the runtime does not automatically add it to every event. |
tags |
(dict only) | {} |
Reserved deployment metadata available to adapters; the runtime does not automatically add it to every event. |
dev_mode |
(dict only) | False |
Allows explicitly configured plain ws:// for non-loopback hosts. Z4J_DEV_MODE from the process environment is deliberately ignored; set settings.Z4J["dev_mode"] in code. Loopback hosts are allowed without this flag. |
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 |
(dict only) | ~/.z4j/buffer-{pid}.sqlite |
Explicit code-level path for the on-disk event buffer. Operators should relocate all per-host state with Z4J_HOME; the removed Z4J_BUFFER_PATH variable causes startup refusal. If the default state directory is not writable, the agent falls back to $TMPDIR/z4j-{uid}/buffer-{pid}.sqlite (mode 0700) and logs a warning. 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, avoiding a short-lived ghost connection from the supervisor process). - 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_ready, after Celery has forked its pool. It imports the Celery engine adapter and calls install_agent(engines=[engine]). The engine attaches to Celery's task signals (task_prerun, task_postrun, task_retry, etc.). It is deliberately not started from worker_init, because that signal fires before the fork.
z4j-django eagerly imports z4j_celery at module-load time so the worker_ready signal handler is wired, even though z4j-django itself doesn't start the agent under Celery.
Shutdown
Section titled “Shutdown”The shutdown hook stops the runtime within a bounded timeout and closes its local buffer and WebSocket. Unsent rows remain in the durable buffer for recovery on a later start; process shutdown does not guarantee network delivery.
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 checks the buffer directory and brain DNS / TCP / TLS path, then starts a temporary WebSocket runtime without leaving a persistent agent behind. Use it to diagnose local and network 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 z4j 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. It catches the gunicorn-under-www-data startup failure and DNS, firewall, or certificate mismatches. The WebSocket probe returns when the background runtime starts and does not wait for authenticated hello_ack, so confirm token, project, and HMAC authentication in the agent and brain logs. See service-user deployments.
Multi-process / Gunicorn
Section titled “Multi-process / Gunicorn”Each gunicorn worker opens one WebSocket. Workers may share one agent token when they should appear as workers beneath the same agent: the runtime gives every process a distinct worker_id, and the brain keys connections by (agent_id, worker_id). Separate tokens create separate agents.
Z4J_AGENT_NAME is a display label, not a connection-identity control. Sharing or separating agents is determined by the minted token; per-process visibility comes from the generated worker IDs.
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, the agent 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- a reconnect displaced the older socket for the same(agent_id, worker_id)slot. One handoff can be normal. A persistent loop means the same worker identity is being registered repeatedly; inspect duplicate startup wiring. Ordinary multi-process workers may share a token because their generated worker IDs differ.- Schedule CRUD disabled - requires
django-celery-beatwithDatabaseScheduler. Filesystem schedulers are read-only.
See engines and z4j and agents for more.