Getting Started

blitz is a micro web framework for Zig focused on raw performance and a clean API. Add it to your project in two steps.

Installation

Add blitz to your build.zig.zon:

zig fetch --save "https://github.com/BennyFranciscus/blitz/archive/main.tar.gz"

Then in your build.zig:

const blitz_dep = b.dependency("blitz", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("blitz", blitz_dep.module("blitz"));
exe.linkLibC(); // required for signal handling and io_uring

Quick Start

const std = @import("std");
const blitz = @import("blitz");

fn hello(_: *blitz.Request, res: *blitz.Response) void {
    _ = res.text("Hello, World!");
}

fn greet(req: *blitz.Request, res: *blitz.Response) void {
    const name = req.params.get("name") orelse "stranger";
    _ = res.text(name);
}

pub fn main() !void {
    var router = blitz.Router.init(std.heap.c_allocator);
    router.get("/", hello);
    router.get("/hello/:name", greet);

    var server = blitz.Server.init(&router, .{ .port = 8080 });
    try server.listen();
}

Build and run:

zig build -Doptimize=ReleaseFast
./zig-out/bin/your-app

Router

blitz uses a radix-trie router for fast path matching with support for path parameters, wildcards, and custom 404 handlers.

var router = blitz.Router.init(allocator);

// HTTP methods
router.get("/path", handler);
router.post("/path", handler);
router.put("/path", handler);
router.delete("/path", handler);
router.patch("/path", handler);
router.head("/path", handler);
router.options("/path", handler);
router.route(.PATCH, "/path", handler);

// Path parameters
router.get("/users/:id", getUserHandler);

// Wildcards
router.get("/static/*filepath", staticHandler);

// Custom 404 (or use the built-in JSON one)
router.notFound(blitz.jsonNotFoundHandler);

Request API

fn handler(req: *blitz.Request, res: *blitz.Response) void {
    // Method
    if (req.method == .GET) { ... }

    // Path parameters
    const id = req.params.get("id") orelse "unknown";

    // Simple query parameter lookup (zero-copy)
    const page = req.queryParam("page") orelse "1";

    // Structured query parsing with typed access
    const q = req.queryParsed();
    const limit = q.getInt("limit", i64) orelse 20;
    const asc = q.getBool("asc") orelse true;
    _ = limit;
    _ = asc;

    // URL-decoded query param
    var decode_buf: [256]u8 = undefined;
    const search = q.getDecode("q", &decode_buf);
    _ = search;

    // Headers
    const ct = req.headers.get("Content-Type");

    // Body
    if (req.body) |body| { ... }
}

Response API

fn handler(_: *blitz.Request, res: *blitz.Response) void {
    // Plain text
    _ = res.text("hello");

    // JSON (raw string)
    _ = res.json("{\"ok\":true}");

    // HTML
    _ = res.html("<h1>Hello</h1>");

    // Custom status
    _ = res.setStatus(.not_found).text("Not Found");

    // Custom headers
    res.headers.set("X-Custom", "value");

    // Pre-computed raw response (maximum performance)
    _ = res.rawResponse("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok");
}

Redirects

fn handler(_: *blitz.Request, res: *blitz.Response) void {
    // Temporary redirect (302)
    _ = res.redirectTemp("/login");

    // Permanent redirect (301)
    _ = res.redirectPerm("/new-url");

    // Custom status redirect
    _ = res.redirect("/other", .found);
}

Middleware

Middleware functions return true to continue or false to short-circuit (e.g., deny access).

Global Middleware

Runs on every request:

// Use the built-in CORS middleware
router.use(blitz.Cors.permissive());

// Or write your own
fn timing(_: *blitz.Request, res: *blitz.Response) bool {
    res.headers.set("Server-Timing", "middleware");
    return true;
}
router.use(timing);

Per-Route Middleware

Runs only on matching routes:

fn auth(req: *blitz.Request, res: *blitz.Response) bool {
    if (req.headers.get("Authorization") == null) {
        blitz.unauthorized(res, "Token required");
        return false; // stop here
    }
    return true;
}

// Attach middleware to a path prefix — applies to all routes under it
router.useAt("/api", auth);

// Or attach middleware to a route group (same effect, cleaner API)
const api = router.group("/api/v1");
api.use(auth);           // Only /api/v1/* routes run this
api.get("/users", listUsers);
api.get("/users/:id", getUser);

// Nested groups stack middleware
const admin = api.group("/admin");
admin.use(adminOnly);    // Runs auth + adminOnly for /api/v1/admin/*
admin.get("/stats", adminStats);

Execution order: global middleware → per-route middleware (collected along the matched path) → handler.

Middleware on parent paths runs before middleware on child paths, so you can layer auth, logging, rate limiting at different levels of your route tree.

Route Groups

Groups share a URL prefix — great for versioned APIs. Prefix concatenation happens at init time with zero runtime overhead.

const api = router.group("/api/v1");
api.get("/users", listUsers);       // matches /api/v1/users
api.get("/users/:id", getUser);     // matches /api/v1/users/:id
api.post("/users", createUser);     // matches /api/v1/users

// Nested groups
const admin = api.group("/admin");
admin.get("/stats", adminStats);    // matches /api/v1/admin/stats

JSON Builder

Zero-allocation JSON serialization powered by comptime. Writes directly into caller-provided buffers.

Struct Serialization

var buf: [512]u8 = undefined;
const json_str = blitz.Json.stringify(&buf, .{
    .name = "Alice",
    .age = @as(i64, 30),
    .active = true,
}) orelse return error.BufferOverflow;
_ = res.json(json_str);

Manual Object Building

var obj_buf: [256]u8 = undefined;
var obj = blitz.JsonObject.init(&obj_buf);
obj.field("id", @as(i64, 1));
obj.field("name", "Alice");
obj.field("tags", @as([]const []const u8, &.{ "admin", "user" }));
const body = obj.finish() orelse "{}";
_ = res.json(body);

Array Building

var arr_buf: [256]u8 = undefined;
var arr = blitz.JsonArray.init(&arr_buf);
arr.push(@as(i64, 1));
arr.push(@as(i64, 2));
arr.push(@as(i64, 3));
const list = arr.finish() orelse "[]";

Supports: structs, slices, ints, floats, bools, strings, optionals (null fields skipped), enums (as strings).

JSON Parsing

Comptime-powered JSON deserialization — parse request bodies directly into Zig structs. Zero-copy strings (slices into the original JSON input).

Basic Usage

const CreateUser = struct {
    name: []const u8,
    email: []const u8,
    age: i64,
};

fn createUser(req: *blitz.Request, res: *blitz.Response) void {
    const user = req.jsonParse(CreateUser) orelse {
        blitz.badRequest(res, "Invalid JSON body");
        return;
    };
    // user.name, user.email, user.age are ready to use
    _ = res.setStatus(.created).json("{\"created\":true}");
}

Optional Fields & Defaults

const UpdateUser = struct {
    name: ?[]const u8 = null,     // missing key → null
    age: ?i64 = null,             // explicit null → null
    role: []const u8 = "user",    // missing key → "user"
};

const update = req.jsonParse(UpdateUser) orelse return;
if (update.name) |name| { /* name was provided */ }

Nested Structs

const Address = struct { city: []const u8, zip: []const u8 };
const Person = struct { name: []const u8, address: Address };

// Parses: {"name":"Alice","address":{"city":"NYC","zip":"10001"}}
const person = req.jsonParse(Person) orelse return;

Enums

const Status = enum { active, inactive, pending };
const Filter = struct { status: Status };

// Parses: {"status":"active"}
const filter = req.jsonParse(Filter) orelse return;

JSON Arrays

const IntList = blitz.JsonArrayParser(i64, 32);  // max 32 items
const nums = IntList.parse("[1, 2, 3, 4, 5]") orelse return;
for (nums.slice()) |n| { /* ... */ }

// Array of structs
const Item = struct { id: i64, name: []const u8 };
const Items = blitz.JsonArrayParser(Item, 16);
const items = Items.parse(req.body orelse "[]") orelse return;

Standalone Parsing

// Parse any JSON string (not just request bodies)
const Config = struct { host: []const u8, port: i64 };
const cfg = blitz.JsonParser.parse(Config, json_string) orelse return;

Unknown JSON keys are silently skipped. Escaped strings (\n, \t, \uXXXX, etc.) are unescaped automatically.

Query Parsing

Structured query string parsing with typed access, URL decoding, multi-value support, and iteration.

fn search(req: *blitz.Request, res: *blitz.Response) void {
    const q = req.queryParsed(); // GET /search?q=hello+world&page=2&debug

    // Simple string lookup (raw, no decoding)
    const term = q.get("q");                          // "hello+world"

    // URL-decoded value
    var buf: [256]u8 = undefined;
    const decoded = q.getDecode("q", &buf);           // "hello world"
    _ = decoded;

    // Typed access
    const page = q.getInt("page", i64) orelse 1;      // 2
    const debug = q.getBool("debug") orelse false;     // false
    _ = page;
    _ = debug;

    // Check key existence (even without value)
    if (q.has("debug")) { ... }

    // Multi-value params: /search?tag=zig&tag=http&tag=fast
    var tags: [8][]const u8 = undefined;
    const n = q.getAll("tag", &tags);                  // n=3
    _ = n;

    // Iterate all params
    var it = q.iterator();
    while (it.next()) |param| {
        // param.key, param.value
        _ = param;
    }

    _ = res.json("{\"ok\":true}");
}

Standalone URL Decoding

var buf: [256]u8 = undefined;
const decoded = blitz.urlDecode(&buf, "hello%20world+foo"); // "hello world foo"

Body Parsing

Parse form bodies and multipart uploads with zero-copy efficiency.

URL-Encoded Forms

fn handleForm(req: *blitz.Request, res: *blitz.Response) void {
    const form = req.formData(); // parses application/x-www-form-urlencoded

    const name = form.get("name") orelse "anonymous";
    const age = form.getInt("age", i64) orelse 0;
    _ = age;

    // URL-decoded values
    var buf: [256]u8 = undefined;
    const msg = form.getDecode("message", &buf);
    _ = msg;

    _ = res.text(name);
}

Multipart/Form-Data (File Uploads)

fn handleUpload(req: *blitz.Request, res: *blitz.Response) void {
    const mp = req.multipart() orelse {
        blitz.badRequest(res, "Expected multipart body");
        return;
    };

    // Get a text field
    if (mp.get("title")) |part| {
        // part.data is the field value
        _ = part;
    }

    // Get a file upload
    if (mp.getFile("avatar")) |file| {
        // file.filename  — original filename
        // file.content_type — MIME type
        // file.data — file contents (slice into request body)
        _ = file;
    }

    _ = res.text("uploaded");
}

Content Type Detection

fn handler(req: *blitz.Request, res: *blitz.Response) void {
    switch (req.contentType()) {
        .json => { /* parse JSON body */ },
        .form_urlencoded => { const form = req.formData(); _ = form; },
        .multipart => { const mp = req.multipart(); _ = mp; },
        else => { blitz.badRequest(res, "Unsupported content type"); return; },
    }
    _ = res.text("ok");
}

Cookies

Zero-copy request cookie parsing and full RFC 6265 response cookie support.

Reading Cookies

fn handler(req: *blitz.Request, res: *blitz.Response) void {
    // Get a single cookie value
    const session = req.cookie("session") orelse "none";
    _ = session;

    // Parse all cookies into a CookieJar
    const jar = req.cookies();
    if (jar.has("theme")) {
        const theme = jar.get("theme").?;
        _ = theme;
    }

    // Iterate all cookies
    var it = jar.iterator();
    while (it.next()) |c| {
        // c.name, c.value
        _ = c;
    }

    _ = res.text("ok");
}

Setting Cookies

fn login(_: *blitz.Request, res: *blitz.Response) void {
    var buf: [256]u8 = undefined;
    _ = res.setCookie(&buf, "session", "tok_abc123", .{
        .max_age = 86400,       // 24 hours
        .path = "/",
        .domain = "example.com",
        .secure = true,
        .http_only = true,
        .same_site = .lax,      // .strict, .lax, or .none
    });
    _ = res.json("{\"ok\":true}");
}

fn logout(_: *blitz.Request, res: *blitz.Response) void {
    var buf: [256]u8 = undefined;
    _ = res.deleteCookie(&buf, "session", .{ .path = "/" });
    _ = res.json("{\"logged_out\":true}");
}

Static Files

Serve files from disk with automatic MIME type detection, directory traversal protection, and optional cache control.

// Serve files from ./public at /static/*
router.staticDir("/static", "./public", .{});

// With options
router.staticDir("/assets", "./dist", .{
    .cache_control = "public, max-age=31536000",  // immutable assets
    .index = true,                                  // serve index.html for directories
    .max_file_size = 10 * 1024 * 1024,             // 10MB max
});

Features:

  • 40+ MIME types — HTML, CSS, JS, images, fonts, media, archives, WASM
  • Path traversal protection — rejects ../, absolute paths, null bytes
  • Directory index — automatically serves index.html for directory paths
  • Cache-Control — optional header for browser caching
  • GET/HEAD only — other methods fall through to route matching

WebSocket

Full RFC 6455 WebSocket support for building real-time applications.

const blitz = @import("blitz");
const ws = blitz.WebSocket;

fn handleWsUpgrade(req: *blitz.Request, res: *blitz.Response) void {
    // Check if this is a WebSocket upgrade request
    if (!ws.isUpgradeRequest(req)) {
        _ = res.setStatus(.bad_request).text("Expected WebSocket upgrade");
        return;
    }

    // Get the client's key
    const key = req.header("Sec-WebSocket-Key") orelse return;

    // Build and send the 101 Switching Protocols response
    var buf: [512]u8 = undefined;
    const upgrade_resp = ws.buildUpgradeResponse(&buf, key, null) orelse return;
    _ = res.rawResponse(upgrade_resp);

    // After upgrade, use ws.parseFrame() and ws.buildFrame() for communication
}

Frame operations:

  • ws.parseFrame(data) — parse incoming frames (auto-unmasks client data)
  • ws.buildFrame(buf, opcode, payload, fin) — build outgoing frames (text, binary, ping, pong)
  • ws.buildCloseFrame(buf, code, reason) — build close frame with status code

Opcodes: .text, .binary, .close, .ping, .pong, .continuation

Close codes: .normal, .going_away, .protocol_error, .too_large, etc.

Compression

blitz automatically compresses responses with gzip or deflate when the client supports it. Compression is enabled by default.

var server = blitz.Server.init(&router, .{
    .port = 8080,
    .compression = true,  // default — automatic gzip/deflate
});

// Disable compression (e.g., for benchmarks)
var server2 = blitz.Server.init(&router, .{
    .compression = false,
});

How it works:

  • After each handler runs, blitz checks Accept-Encoding and compresses the body if beneficial
  • Uses Zig's std.compress.gzip (fast level) for minimal latency overhead
  • Adds Content-Encoding: gzip and Vary: Accept-Encoding headers automatically
  • If compressed output is larger than the original, compression is skipped
  • Pre-computed rawResponse() calls bypass compression (benchmark fast path)
  • Minimum body size: 256 bytes

Manual Compression

const blitz = @import("blitz");

fn handler(req: *blitz.Request, res: *blitz.Response) void {
    _ = res.json(large_json_string);

    // Check if compression is worthwhile
    if (blitz.shouldCompress(req, res)) {
        var buf: [65536]u8 = undefined;
        _ = blitz.compressResponse(&buf, req, res);
    }
}

Rate Limiting

Token-bucket rate limiter with per-IP tracking and standard HTTP headers.

// Initialize the rate limiter
var limiter = try blitz.RateLimiter.init(allocator, .{
    .max_requests = 100,  // requests per window
    .window_secs = 60,    // window duration
    .max_clients = 4096,  // max tracked IPs
});

// Use in handlers
fn myHandler(req: *blitz.Request, res: *blitz.Response) void {
    if (!blitz.RateLimit.allow(&limiter, req, res)) return;
    // ... handle request normally
}

The rate limiter extracts client IPs from X-Forwarded-For or X-Real-IP headers (for reverse proxy setups), and sets X-RateLimit-Remaining and Retry-After headers automatically.

CORS

Built-in CORS middleware with configurable origins, methods, and preflight handling.

// Permissive CORS — allow all origins
router.use(blitz.Cors.permissive());

// Configured CORS — specific origins with credentials
router.use(blitz.Cors.middleware(.{
    .origins = &.{ "https://myapp.com", "http://localhost:3000" },
    .allow_credentials = true,
    .headers = "Content-Type, Authorization, X-Request-ID",
    .max_age = 3600,
    .max_age_str = "3600",
}));

The CORS middleware automatically handles OPTIONS preflight requests (returns 204 with appropriate headers) and sets Access-Control-Allow-Origin, Vary, and Access-Control-Allow-Credentials headers on all responses.

When allow_credentials is true, the middleware echoes the request's Origin header instead of using * (as required by the spec).

Context Injection

Pass application state (database connections, config, services) to handlers without global variables.

const AppState = struct {
    db: *Database,
    config: *AppConfig,
};

fn getUsers(req: *blitz.Request, res: *blitz.Response) void {
    const app = req.context(AppState);
    const users = app.db.query("SELECT * FROM users");
    // ...
}

pub fn main() !void {
    var state = AppState{ .db = &db, .config = &config };
    var router = blitz.Router.init(std.heap.c_allocator);
    router.get("/users", getUsers);

    var server = blitz.Server.init(&router, .{
        .port = 8080,
        .context = @ptrCast(&state),
    });
    try server.listen();
}

The context is set once via Server.Config.context and automatically injected into every request. Access it in any handler with req.context(T) which returns a typed pointer.

Works with both epoll and io_uring backends. The context pointer is shared across all threads — ensure your state is either read-only or thread-safe (use atomics, mutexes, or per-thread copies).

Error Handling

Structured JSON error responses with convenience helpers.

fn getUser(req: *blitz.Request, res: *blitz.Response) void {
    const id = req.params.get("id") orelse {
        blitz.badRequest(res, "Missing user ID");
        return;
    };
    // ... look up user ...
    blitz.notFound(res, "User not found");
}

// Available error helpers:
blitz.badRequest(res, "message");      // 400
blitz.unauthorized(res, "message");    // 401
blitz.forbidden(res, "message");       // 403
blitz.notFound(res, "message");        // 404
blitz.methodNotAllowed(res, "msg");    // 405
blitz.internalError(res, "message");   // 500

// Generic:
blitz.sendError(res, .bad_request, "Custom message");

// Response format: {"error":{"status":400,"message":"Missing user ID"}}

// Built-in JSON 404 handler for the router:
router.notFound(blitz.jsonNotFoundHandler);

Logging

Built-in structured request logging with zero allocations — writes directly to stderr.

var server = blitz.Server.init(&router, .{
    .port = 8080,
    .logging = .{
        .enabled = true,
        .format = .text,          // .text or .json
        .min_level = .info,       // .debug, .info, .warn, .err, .off
        .slow_threshold_ms = 500, // warn on slow requests (0 = disabled)
    },
});

Text Format

INFO  GET /api/users?page=1 200 1.2ms 256B
WARN  POST /api/login 401 0.3ms 45B
ERROR GET /crash 500 15.7ms 128B

JSON Format

{"level":"INFO","method":"GET","path":"/api/users","query":"page=1","status":200,"latency_us":1200,"size":256}

Log levels are auto-determined from response status: 5xx → ERROR, 4xx → WARN, 2xx/3xx → INFO.

Slow request detection: Set slow_threshold_ms to log requests exceeding the threshold even if their status-based level is below min_level.

General-Purpose Logging

blitz.logMsg(config, .info, "server started on port 8080");
blitz.logMsg(config, .warn, "connection pool exhausted");

Logging is disabled by default — zero overhead when not configured. All formatting uses stack buffers (no heap allocations).

Graceful Shutdown

blitz handles SIGTERM and SIGINT automatically:

  1. Stops accepting new connections immediately
  2. Finishes in-flight requests and flushes responses
  3. Sends Connection: close to signal clients
  4. Drains for up to shutdown_timeout seconds
  5. Force-closes remaining connections if timeout expires

Works correctly as PID 1 in Docker containers. Check blitz.isShuttingDown() in middleware or handlers to detect shutdown in progress.

// Middleware that rejects new work during shutdown
fn shutdownAware(req: *blitz.Request, res: *blitz.Response) bool {
    if (blitz.isShuttingDown()) {
        _ = res.setStatus(.service_unavailable).text("Shutting down");
        return false;
    }
    return true;
}

Server Configuration

var server = blitz.Server.init(&router, .{
    .port = 8080,
    .threads = null,          // auto-detect CPU count
    .keep_alive_timeout = 60, // seconds (0 = disable)
    .shutdown_timeout = 30,   // seconds to drain before force-close
});
try server.listen(); // Blocks until SIGTERM/SIGINT

SQLite Integration

Built-in SQLite wrapper via @cImport — zero overhead, per-thread connections, prepared statements with typed column access.

Opening a Database

const blitz = @import("blitz");

// Open with read-only + mmap for maximum read performance
var db = try blitz.SqliteDb.open("/data/app.db", .{
    .readonly = true,
    .mmap_size = 64 * 1024 * 1024,  // 64MB mmap
    .wal = true,                      // WAL journal mode
});

Prepared Statements

var stmt = try db.prepare(
    "SELECT id, name, price FROM items WHERE price BETWEEN ?1 AND ?2 LIMIT 50"
);
defer stmt.finalize();

// Bind parameters (1-indexed)
try stmt.bindDouble(1, min_price);
try stmt.bindDouble(2, max_price);

// Iterate results
while (try stmt.step()) {
    const id = stmt.columnInt(0);      // i64
    const name = stmt.columnText(1);   // []const u8 (zero-copy, valid until next step/reset)
    const price = stmt.columnDouble(2); // f64
    // ... process row
}

// Reset for reuse (keeps compiled query plan)
stmt.reset();

Column Types

// Type checking
const col_type = stmt.columnType(0); // .integer, .float, .text, .blob, .null_type

// Null checking
if (stmt.columnIsNull(3)) { ... }

// Blob data
const data = stmt.columnBlob(4);  // []const u8

Build Setup

Requires libsqlite3-dev at build time:

// build.zig
exe.linkSystemLibrary("sqlite3");

io_uring Backend

Experimental Linux 5.19+

For maximum throughput, blitz includes an io_uring backend:

// In your main.zig:
var uring_server = blitz.UringServer.init(&router, .{
    .port = 8080,
    .threads = null,       // auto-detect
    .compression = false,  // for benchmarks
});
try uring_server.listen();

Or use the environment variable:

BLITZ_URING=1 ./blitz

Architecture

  • Dedicated acceptor thread — single io_uring ring with multishot accept, distributes connections round-robin
  • SPSC queue fd handoff — lock-free single-producer/single-consumer queue (cache-line aligned) per reactor thread for zero-contention fd distribution
  • Reactor threads — each has its own io_uring ring dedicated to recv/send (no accept overhead)
  • Kernel-managed buffer ring (io_uring_buf_ring) — 4096 pre-allocated recv buffers, zero-SQE buffer recycling via shared memory
  • Zero-copy send (send_zc) — eliminates kernel buffer copy for response writes (kernel 6.0+, auto-fallback)
  • Async send — non-blocking response writes with partial-send resubmission
  • SINGLE_ISSUER + DEFER_TASKRUN — reduced kernel overhead on reactor rings (auto-fallback for older kernels)
  • Connection pooling — per-reactor pre-allocated ConnState pool (4096 slots) with O(1) acquire/release

Requirements: Linux 5.19+ (6.0+ for zero-copy send, 6.1+ for DEFER_TASKRUN). Docker containers need --privileged or appropriate seccomp profile.

Architecture

src/
├── blitz.zig           # Module root — re-exports everything
├── blitz/
│   ├── types.zig       # Request, Response, Method, StatusCode, Headers
│   ├── router.zig      # Radix-trie router with middleware, groups, params & wildcards
│   ├── parser.zig      # Zero-copy HTTP/1.1 request parser
│   ├── server.zig      # Epoll event loop, connection management, graceful shutdown
│   ├── uring.zig       # io_uring backend — acceptor + reactor threads
│   ├── spsc.zig        # Lock-free SPSC queue for cross-thread fd handoff
│   ├── pool.zig        # Connection pool — pre-allocated ConnState objects
│   ├── query.zig       # Query string parser with URL decoding and typed access
│   ├── json.zig        # Comptime JSON serializer (Json, JsonObject, JsonArray)
│   ├── body.zig        # Request body parsing (URL-encoded, multipart)
│   ├── cookie.zig      # Cookie parsing and Set-Cookie builder (RFC 6265)
│   ├── compress.zig    # Response compression (gzip/deflate)
│   ├── errors.zig      # Structured error responses
│   ├── static.zig      # Static file serving (MIME detection, path security)
│   ├── websocket.zig   # WebSocket frames, handshake, close codes (RFC 6455)
│   └── tests.zig       # Unit tests for all modules
├── main.zig            # HttpArena benchmark entry point
examples/
└── hello.zig           # Example app with all features

Design Decisions

  • No allocations in hot path — responses written to pre-allocated buffers
  • Edge-triggered epoll — fewer syscalls than level-triggered
  • SO_REUSEPORT — kernel distributes connections across worker threads
  • Pre-computed responses — full HTTP response built at startup for static data
  • Radix trie over hash map — better cache locality for path matching
  • Layered middleware — global + per-route; fn(*Req, *Res) bool is simpler and faster than callback chains
  • Route groups — prefix concatenation at init time, zero runtime overhead
  • Comptime JSON — Zig's comptime introspects struct fields at compile time, no reflection cost at runtime
  • Connection pool — pre-allocated ConnState per worker thread, O(1) acquire/release
  • Keep-alive timeout — timerfd-based idle connection sweep
  • Body discard mode — large uploads (>64KB) are counted but not buffered, reducing memory from O(body_size × connections) to near-zero
  • SIMD HTTP parsing@Vector(16, u8) accelerated scanning for header boundaries and byte search

Benchmarks

Benchmarked on HttpArena — 64-core AMD Threadripper, dedicated hardware, io_uring backend.

ProfileConnectionsThroughputMemory
Baseline40963.06M req/s4.1 GiB
Baseline163842.77M req/s4.2 GiB
Pipelined (p=16)409638.9M req/s4.1 GiB
JSON (8.4KB body)163841.66M req/s3.8 GiB
Upload (20MB body)641,117 req/s4.1 GiB
Compression (gzip)409695K req/s4.5 GiB
Noisy (mixed traffic)40961.99M req/s4.1 GiB
Limited-conn (r=10)40961.38M req/s4.1 GiB
WebSocket echo (p=16)409650.2M msg/s4.1 GiB

#3 on HttpArena baseline, behind ringzero (C, io_uring) at 3.43M and h2o (C) at 3.16M. Ahead of nginx (3.03M), hyper/Rust (2.94M), actix/Rust (2.71M), and drogon/C++ (2.25M).