Skip to content

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/performance are disabled
  • Static file serving defers to the reverse proxy
app.run(host="0.0.0.0", port=8000, prod=True)

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:

  1. Connection accept -- TCP connection accepted by the thread pool
  2. HTTP parsing -- request line, headers, and body parsed in Zig
  3. Size validation -- body size checked against max_body_size (default 10 MB)
  4. Routing -- radix trie lookup to find matching handler
  5. Parameter extraction -- path parameters extracted and type-converted
  6. Middleware chain -- request passes through middleware stack
  7. 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-alive in 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:

python manage.py runziserver 0.0.0.0:8000

The ZigHandler bridges the Zig HTTP server to Django's WSGI interface:

  1. Zig server accepts the connection and parses the HTTP request
  2. The request is converted to a WSGI environ dict
  3. Django's WSGI handler processes the request
  4. 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)
# In Django settings.py
INSTALLED_APPS = [
    ...
    'hyperdjango.serving',
]

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

  1. Signal handler sets atomic shutdown_flag and writes to self-pipe
  2. Accept loop's poll() wakes and breaks (stops accepting new connections)
  3. Worker threads finish their current request (tracked via active_requests counter)
  4. Drain timeout: 30 seconds for in-flight requests to complete
  5. Worker threads exit (queue returns null on shutdown, condition variable broadcast)
  6. on_shutdown hooks called (Python-side cleanup)
  7. PID file removed
  8. Clean exit (exit code 0)
@app.on_shutdown
async def cleanup():
    await db.close()
    logger.info("Server shutdown complete")

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=exec with KillSignal=SIGTERM for graceful shutdown
  • TimeoutStopSec=30 matching the drain timeout
  • Restart=on-failure with 5-second delay
  • Security hardening: PrivateTmp, ProtectSystem=strict, NoNewPrivileges
  • Resource limits: LimitNOFILE=65536, LimitNPROC=4096
  • KillMode=mixed to 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:

# asgi.py
from myapp import app

# HyperApp implements the ASGI protocol
application = app
uvicorn asgi:application --host 0.0.0.0 --port 8000

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.