Server¶
Native Zig HTTP server -- 2.1x faster than uvicorn (13k vs 6k req/s).
Architecture¶
The HyperDjango server is a compiled Zig native extension that handles HTTP parsing, routing, WebSocket upgrades, and response serialization entirely in native code. Key architectural components:
- 24-thread pool -- pre-spawned OS threads handle connections concurrently without the GIL
- Radix trie router -- O(log n) route matching with 528 ns per resolve for dynamic routes
- Zero-copy request parsing -- HTTP headers and body parsed without unnecessary allocations
- Native response builder -- responses serialized directly to socket buffers
- RFC 6455 WebSocket -- full WebSocket support with SIMD XOR unmasking
The server is designed for Python 3.14t (free-threaded) and runs request handlers without GIL contention.
Quick Start¶
from hyperdjango import HyperApp
app = HyperApp("myapp")
@app.get("/")
async def index(request):
return {"message": "Hello!"}
@app.post("/users")
async def create_user(request):
data = request.json
return {"created": data["name"]}, 201
app.run(host="0.0.0.0", port=8000)
Server Configuration¶
app.run() Parameters¶
app.run(
host="0.0.0.0", # Bind address (default: "127.0.0.1")
port=8000, # Bind port (default: 8000)
prod=False, # Production mode (disables debug pages, enables optimizations)
)
| Parameter | Default | Description |
|---|---|---|
host |
"127.0.0.1" |
Network interface to bind to. Use "0.0.0.0" for all interfaces. |
port |
8000 |
TCP port to listen on |
prod |
False |
Production mode -- disables debug error pages, enables security headers |
Production Mode¶
When prod=True:
- Detailed error pages are replaced with generic
{"detail": "Internal Server Error"} - Security headers are applied (HSTS, X-Frame-Options, etc.)
- Debug endpoints like
/debug/performanceare disabled - Static file serving defers to the reverse proxy
Routing¶
HTTP Methods¶
@app.get("/users")
async def list_users(request):
...
@app.post("/users")
async def create_user(request):
...
@app.put("/users/{id:int}")
async def update_user(request, id):
...
@app.patch("/users/{id:int}")
async def patch_user(request, id):
...
@app.delete("/users/{id:int}")
async def delete_user(request, id):
...
Path Parameters with Type Conversion¶
# Integer parameter -- auto-converted to int
@app.get("/users/{id:int}")
async def get_user(request, id): # id is already an int
...
# String parameter (default)
@app.get("/users/{username:str}")
async def get_by_name(request, username):
...
# Slug parameter (letters, numbers, hyphens)
@app.get("/articles/{slug:slug}")
async def get_article(request, slug):
...
# UUID parameter
@app.get("/items/{item_id:uuid}")
async def get_item(request, item_id): # item_id is a UUID string
...
# Path parameter (catches remaining path segments)
@app.get("/files/{path:path}")
async def serve_file(request, path): # path = "docs/guide/intro.md"
...
Supported parameter types:
| Type | Pattern | Example Match |
|---|---|---|
str |
[^/]+ |
hello |
int |
[0-9]+ |
42 |
slug |
[a-zA-Z0-9-]+ |
my-article |
uuid |
UUID v4 | 550e8400-e29b-41d4-a716-446655440000 |
path |
.+ |
docs/guide/intro.md |
Multiple Methods¶
@app.route("/items", methods=["GET", "POST"])
async def items(request):
if request.method == "GET":
return await list_items()
else:
return await create_item(request)
URL Namespaces & Includes¶
Organize routes with routers and namespaces:
from hyperdjango.router import Router
blog = Router()
blog.get("/", list_posts, name="list")
blog.get("/{id:int}", post_detail, name="detail")
blog.post("/{id:int}/comment", add_comment, name="comment")
api = Router()
api.get("/users", list_users, name="users")
api.get("/stats", get_stats, name="stats")
# Mount routers with namespaces
app.router.include("/blog", blog, namespace="blog")
app.router.include("/api/v1", api, namespace="api")
# Reverse URL resolution
app.router.reverse("blog:detail", id=42) # "/blog/42"
app.router.reverse("blog:list") # "/blog/"
app.router.reverse("api:users") # "/api/v1/users"
Radix Trie Router¶
The router uses a native Zig radix trie data structure for route matching. This provides O(log n) lookups regardless of the number of routes, with benchmark performance of 528 ns per resolve for dynamic routes with parameters.
Static routes (no parameters) are resolved even faster via exact hash map lookup.
Request Object¶
request.method # "GET", "POST", "PUT", "PATCH", "DELETE"
request.path # "/users/42"
request.query # {"page": "1", "sort": "name"}
request.json # Parsed JSON body (lazy)
request.form # Parsed form data (lazy)
request.headers # Dict of headers (case-insensitive keys)
request.cookies # Dict of cookies (native Zig parser)
request.client_ip # Client IP address (respects X-Forwarded-For)
request.is_secure # True if HTTPS (checks X-Forwarded-Proto)
request.text # Raw body as string
request.bytes # Raw body as bytes
The request object uses lazy parsing -- request.json, request.form, and request.cookies are only parsed when first accessed. Cookie parsing uses the native Zig cookie parser with percent-decoding, benchmarking faster than Python's http.cookies module.
Response Builder¶
from hyperdjango.response import Response
# JSON response (most common)
Response.json({"key": "value"})
Response.json(data, status=201)
# HTML response
Response.html("<h1>Hello</h1>")
Response.html(rendered_template, status=200)
# Plain text
Response.text("plain text")
Response.text("Not Found", status=404)
# Redirect
Response.redirect("/other") # 302 temporary
Response.redirect("/other", status=301) # 301 permanent
# File download
Response.file("/path/to/file.pdf")
Response.attachment("/path/to/file.pdf", filename="report.pdf")
# Streaming response
Response.stream(async_generator)
# Server-Sent Events
Response.sse(event_generator)
Response objects support headers, cookies, and caching:
response = Response.json({"ok": True})
response.headers["X-Custom"] = "value"
response.headers["Set-Cookie"] = "session=abc; HttpOnly; Secure"
The native response builder serializes headers and body directly to the socket buffer, avoiding intermediate string concatenation.
Middleware¶
from hyperdjango.standalone_middleware import (
CORSMiddleware,
SecurityMiddleware,
TimingMiddleware,
LoggingMiddleware,
RateLimitMiddleware,
CompressionMiddleware,
CSRFMiddleware,
)
# CORS
app.use(CORSMiddleware(
origins=["https://mysite.com"],
methods=["GET", "POST", "PUT", "DELETE"],
headers=["Authorization", "Content-Type"],
max_age=3600,
))
# Security headers (HSTS, X-Frame-Options, etc.)
app.use(SecurityMiddleware(hsts=True, frame_deny=True))
# Request timing (adds X-Response-Time header)
app.use(TimingMiddleware())
# Access logging
app.use(LoggingMiddleware())
# Rate limiting
app.use(RateLimitMiddleware(rate="100/minute"))
# Response compression (gzip)
app.use(CompressionMiddleware(min_size=1024))
# CSRF protection
app.use(CSRFMiddleware(secret="your-secret"))
Middleware executes in the order it is registered. Each middleware receives the request and a call_next function:
async def custom_middleware(request, call_next):
# Before handler
start = time.time()
response = await call_next(request)
# After handler
elapsed = time.time() - start
response.headers["X-Custom-Time"] = f"{elapsed:.3f}s"
return response
app.use(custom_middleware)
WebSocket Support¶
WebSocket connections are handled with full RFC 6455 compliance. The Zig server performs the HTTP upgrade handshake and SIMD XOR unmasking of frames natively.
@app.websocket("/ws")
async def ws_handler(ws):
# Receive messages
async for message in ws:
# Echo back
await ws.send(f"Echo: {message}")
WebSocket API¶
@app.websocket("/ws/chat")
async def chat(ws):
# Send text
await ws.send("Hello!")
# Send JSON
await ws.send_json({"type": "greeting", "message": "Hello!"})
# Receive with timeout
message = await ws.receive(timeout=30)
# Close with code and reason
await ws.close(code=1000, reason="Normal closure")
SIMD XOR Unmasking¶
WebSocket frames from clients are masked per RFC 6455. The Zig implementation uses SIMD operations to XOR-unmask 16 bytes at a time, significantly faster than byte-by-byte unmasking.
Request Validation Pipeline¶
Incoming requests pass through a validation pipeline before reaching the route handler:
- Connection accept -- TCP connection accepted by the thread pool
- HTTP parsing -- request line, headers, and body parsed in Zig
- Size validation -- body size checked against
max_body_size(default 10 MB) - Routing -- radix trie lookup to find matching handler
- Parameter extraction -- path parameters extracted and type-converted
- Middleware chain -- request passes through middleware stack
- Handler execution -- route handler called with validated request
If any step fails, an appropriate HTTP error response is returned immediately without invoking downstream handlers.
Connection Keep-Alive¶
The server supports HTTP/1.1 keep-alive connections by default. Multiple requests can be sent over the same TCP connection, reducing connection establishment overhead.
Keep-alive behavior:
- Connections are kept alive unless the client sends
Connection: close - Idle connections are reaped after a configurable timeout
- The server sends
Connection: keep-alivein responses
Static File Serving¶
In development, the server can serve static files directly:
app = HyperApp("myapp", static="static/")
# Files in static/ are served at /static/
# /static/css/style.css -> static/css/style.css
For production, use a reverse proxy (nginx) for static files. The StaticFilesMiddleware handles ETag generation, Cache-Control headers, gzip compression, and If-None-Match/If-Modified-Since conditional requests.
See the Static Files documentation for details on collectstatic, manifest hashing, and CDN integration.
Hot Reload¶
In development, the server watches for file changes and automatically reloads:
from hyperdjango.hot_reload import HotReloader
reloader = HotReloader(
watch_dirs=["myapp/"],
extensions=[".py", ".html"],
)
reloader.start()
The file watcher uses native OS APIs:
- macOS: kqueue (no polling)
- Linux: inotify (no polling)
When a change is detected, affected modules are reloaded with importlib.reload() and an SSE event is pushed to connected browsers for instant page refresh.
Django WSGI Bridge¶
Run Django applications through the Zig HTTP server for improved performance:
The ZigHandler bridges the Zig HTTP server to Django's WSGI interface:
- Zig server accepts the connection and parses the HTTP request
- The request is converted to a WSGI environ dict
- Django's WSGI handler processes the request
- The Django response is sent back through the Zig server using
sendFullResponse
The bridge supports:
- Full response headers (Set-Cookie, CSRF tokens, etc.)
- Streaming responses
- File responses
- WebSocket upgrade (passed through to ASGI)
Performance¶
The WSGI bridge provides the Zig server's connection handling and parsing performance while running standard Django views. This typically yields 1.5-2x throughput improvement over gunicorn/uvicorn for Django applications due to faster HTTP parsing and connection management.
Graceful Shutdown¶
The Zig server handles shutdown signals natively using a self-pipe + atomic flag architecture. No Python signal handlers needed -- the shutdown is coordinated at the C/Zig level for reliability.
Signal Handling¶
- SIGTERM -- graceful shutdown (systemd, Docker, process managers)
- SIGINT -- graceful shutdown (Ctrl+C in terminal)
Both signals write to a self-pipe that wakes the accept loop immediately (async-signal-safe). The accept loop exits, then the server drains in-flight requests.
Shutdown Sequence¶
- Signal handler sets atomic
shutdown_flagand writes to self-pipe - Accept loop's
poll()wakes and breaks (stops accepting new connections) - Worker threads finish their current request (tracked via
active_requestscounter) - Drain timeout: 30 seconds for in-flight requests to complete
- Worker threads exit (queue returns null on shutdown, condition variable broadcast)
on_shutdownhooks called (Python-side cleanup)- PID file removed
- Clean exit (exit code 0)
Programmatic Shutdown¶
Trigger shutdown from Python code (useful for tests, health checks, or admin endpoints):
from hyperdjango._hyperdjango_native import _server_shutdown
_server_shutdown() # Sets the shutdown flag, server exits after draining
Server Management¶
CLI Commands¶
# Development (foreground)
hyper run --app app:app --port 8000
# Production (background daemon)
hyper start --app app:app --port 8000 --prod
hyper stop --port 8000 # SIGTERM graceful shutdown
hyper restart --app app:app --port 8000 # stop + start
hyper status --port 8000 # Check if running
hyper start writes a PID file (.hyper.<port>.pid) and redirects output to .hyper.<port>.log. hyper stop sends SIGTERM and waits up to 30 seconds for graceful exit, then SIGKILL as fallback.
systemd Integration¶
Generate and install a production systemd unit file:
# Generate unit file (writes to current directory if not root)
hyper systemd install --app app:app --port 8000
# Install and enable as root
sudo hyper systemd install --app app:app --port 8000 --enable
# Manage the service
sudo systemctl status hyperdjango-myapp
sudo systemctl restart hyperdjango-myapp
sudo journalctl -u hyperdjango-myapp -f
# Remove
sudo hyper systemd uninstall
The generated unit file includes:
Type=execwithKillSignal=SIGTERMfor graceful shutdownTimeoutStopSec=30matching the drain timeoutRestart=on-failurewith 5-second delay- Security hardening:
PrivateTmp,ProtectSystem=strict,NoNewPrivileges - Resource limits:
LimitNOFILE=65536,LimitNPROC=4096 KillMode=mixedto clean up worker threads
Docker¶
FROM python:3.14-slim
WORKDIR /app
COPY . .
RUN pip install -e .
RUN hyper-build --release
EXPOSE 8000
CMD ["hyper", "run", "--app", "app:app", "--host", "0.0.0.0", "--port", "8000", "--prod"]
The server handles SIGTERM from Docker's stop command, draining connections before exit.
ASGI Compatibility¶
HyperDjango applications can be served by any ASGI server (uvicorn, hypercorn, daphne) for environments where the native Zig server is not available:
However, the native Zig server is strongly recommended for production -- it provides 2.1x higher throughput and lower latency.
Performance Benchmarks¶
| Metric | Zig Server | uvicorn |
|---|---|---|
| Requests/sec (hello world) | 13,000 | 6,000 |
| Route resolution (dynamic) | 528 ns | N/A |
| HTTP parse overhead | ~2 us | ~10 us |
| WebSocket frame unmask | SIMD (16 bytes/cycle) | byte-by-byte |
| Multipart boundary scan | 20.4 GB/s (SIMD) | ~100 MB/s |
The benchmarks were measured with wrk on a single-machine setup. Real-world performance depends on application logic, database queries, and network conditions.