PRODUCTION.md — Deploying justapi in production
This guide covers the operational, security, and observability settings needed
to run a justapi service in production. It assumes you have already built the
extension (maturin develop / maturin build) and have a working JustAPIApp.
Rust-first: all networking, protocol parsing, routing, middleware, scheduling,
and body limiting live injustapi-core(Rust). Python only provides
application handlers and glue. Tuning below is therefore largely Rust-driven
and exposed through the PythonJustAPIAppAPI.
1. Build for production
# Release build of the Rust extension (optimized, panic=abort)
VIRTUAL_ENV=.venv .venv/bin/maturin develop --release --manifest-path crates/justapi-py/Cargo.toml
Cargo.toml sets panic = "abort" in [profile.release]. On an unexpected
panic the process aborts (no unwinding across the PyO3/GIL FFI, no UB). Run
under a supervisor that restarts the process (systemd, Kubernetes,supervisord, docker restart=always).
2. Process supervision & crash-fast model
Do not wrap request handling in
catch_unwind. It is unsound at the GIL
FFI boundary and costs a large throughput regression. (SeeDECISIONS.md
ADR-053.)Treat the server as crash-fast + restart. A supervisor restarts it within
milliseconds. Design handlers to be idempotent so a restart is safe.systemd unit sketch:
[Service] ExecStart=/srv/app/venv/bin/python -m myapp Restart=always RestartSec=1 # Cap restart storms StartLimitIntervalSec=10 StartLimitBurst=5Kubernetes: set
restartPolicy: Always, alivenessProbeagainst/live,
and areadinessProbeagainst/ready(see §5).
3. TLS termination
justapi speaks plain HTTP/1.1 + HTTP/2 at the socket. Terminate TLS at an
edge proxy (recommended) or enable native TLS:
- Edge proxy (recommended): put
justapibehindnginx,Caddy,Envoy,
or a cloud LB. The proxy handles TLS, HTTP/2, and connection reuse; justapi
binds to a private loopback/Unix socket. This keeps certificates out of the
Rust process and lets you use standard cert tooling (ACME, secret stores). - Native TLS:
Server::with_tls(config)(Rusttlsfeature) is available
if you must terminate in-process. Prefer the proxy pattern for rotation and
OCSP/stapling.
Pass the real client address and scheme from the proxy via X-Forwarded-For
/ X-Forwarded-Proto handling in your middleware so logs and rate-limits see
the true peer.
4. Request body size — DoS hardening
Unbounded request bodies are a memory-exhaustion DoS vector. Every read path is
capped by a configurable limit (ADR-055):
app.run("0.0.0.0:8000", max_body_size=1024 * 1024) # 1 MiB cap
- Default:
50 * 1024 * 1024(50 MiB). - Bodies over the limit are rejected with
413 Payload Too Largeand{"detail":"payload too large"}— they are never buffered into memory. - Tune per deployment, not globally: set a tight cap for JSON/API endpoints
and a larger one only where big uploads are expected. Run separate services
(or routes behind different listeners) if one size does not fit all. - The Rust constant
DEFAULT_MAX_BODY_SIZEis the fallback for the built-inserve()default-routes path.
5. Health, readiness & liveness
GET /health— liveness. Returns200 {"status":"ok"}when the process is
up. Use for KuberneteslivenessProbe.GET /ready— readiness. Returns200only when all registered health
checks pass (DB pool, downstream deps). Returns503otherwise. Use forreadinessProbeso traffic is withheld until dependencies are healthy.GET /live— lightweight liveness alias.Register custom checks:
app.register_health_check("postgres", lambda: db_ok())GET /metrics— Prometheus exposition in text format (real counters, not a
stub). Scrape it with Prometheus; alert onrequesterror rates, p99 latency,
andconnectionsaturation.
6. Security headers (opt-in)
Secure-by-default means no injected headers until you ask. Enable them
explicitly:
app.enable_secure_headers()
This adds Strict-Transport-Security, X-Content-Type-Options,X-Frame-Options, Content-Security-Policy, and Referrer-Policy defaults.
CORS is also secure-by-default (no permissive * unless you configure it).
7. Observability
- Tracing: justapi emits
tracingspans per request (handler.execute,
withhttp.status_code). Pipetracing-subscriber(env-filter / OTel) via
your Python bootstrap to get distributed traces. - Metrics: scrape
/metrics. Key signals: request count by route/status,
p50/p95/p99 latency, inbound/outbound bytes, open connections, and error
ratios. - Logs: structured logs via
tracing; avoid logging request bodies in
production (they may contain PII/secrets). The body cap (§4) bounds memory
even if you do sample payloads.
8. Connection & timeout tuning
- Per-request timeout: requests exceeding the configured timeout return
504 Gateway Timeout(the handler is cancelled). Set it to bound worst-case tail
latency. - Max concurrent connections is semaphore-bounded; tune for your worker count
and memory budget. - Behind a proxy, keep proxy and justapi timeouts aligned (proxy idle timeout ≥
justapi timeout) to avoid spurious 504s.
9. Graceful shutdown
run() installs SIGINT/SIGTERM handlers. On signal it stops accepting new
connections and drains in-flight requests before exiting, so a supervisor
restart loses no committed work. Ensure your supervisor sends SIGTERM (notSIGKILL) for clean drains.
10. Minimal production bootstrap
from justapi import JustAPIApp
app = JustAPIApp()
@app.get("/healthz")
def healthz(request):
return {"status": "ok"}
app.register_health_check("postgres", lambda: db_ok())
app.enable_secure_headers()
if __name__ == "__main__":
# 1 MiB body cap, behind an nginx/LB doing TLS
app.run("127.0.0.1:8000", max_body_size=1024 * 1024)
Front it with:
server {
listen 443 ssl;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 1m; # match justapi's cap
}
}
11. Pre-flight checklist
- Release build (
maturin develop --release). - Supervisor with
Restart=always/restartPolicy: Always. - TLS terminated at proxy (or native TLS configured).
-
max_body_sizeset to the smallest value your endpoints allow. -
enable_secure_headers()called; CORS explicitly configured. -
/live+/readywired to liveness/readiness probes. -
/metricsscraped; alerts on error ratio + p99. - Request timeout set; proxy idle timeout ≥ justapi timeout.
- Graceful shutdown verified (SIGTERM drains cleanly).
- No request bodies logged in production.