karlseguin/http.zig
> A from-scratch HTTP/1.1 server library for Zig — the de facto base layer of > the Zig web ecosystem, deliberately not built on std.http.Server.
Overview
http.zig (module name httpz) is an HTTP/1.1 server library by Karl Seguin, started in March 20231. It exists because std.http.Server is slow and assumes well-behaved clients; most other Zig HTTP servers wrap either the std implementation (inheriting its performance) or C libraries2. httpz is pure Zig with its own socket handling and parser; the author reports ~140K requests/second for a basic request on an Apple M22. Higher-level Zig frameworks — JetZig and Tokamak — build on it2, making httpz roughly what net/http is to Go: the substrate others frame over.
The defining tension is Zig itself. Zig is pre-1.0 and breaks its standard library every release, so httpz maintains a branch per Zig version (zig-0.15, dev, etc.); master tracks the latest stable Zig2. As of mid-2026 master targets Zig 0.16 and the author explicitly labels that port experimental and "not well tested"2. Adopting httpz means adopting this churn: upgrade pain comes from the language more than from the library.
At ~1.6k stars with a single primary maintainer, it is small by web-framework standards but among the most-used Zig libraries, and actively developed.
Getting Started
zig fetch --save "git+https://github.com/karlseguin/http.zig#master"
// build.zig
const httpz = b.dependency("httpz", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("httpz", httpz.module("httpz"));
// Zig 0.16 / master API
const std = @import("std");
const httpz = @import("httpz");
pub fn main(init: std.process.Init) !void {
var server = try httpz.Server(void).init(init.io, init.gpa, .{
.address = .localhost(5882),
}, {});
var router = try server.router(.{});
router.get("/api/user/:id", getUser, .{});
try server.listen(); // blocks
}
fn getUser(req: *httpz.Request, res: *httpz.Response) !void {
try res.json(.{ .id = req.param("id").?, .name = "Teg" }, .{});
}
Architecture / How It Works
Comptime handler generics. The core type is httpz.Server(H) where H is your application handler. With H = void, actions are plain fn(*Request, *Response) !void; with a real handler, the instance is passed to every action (the idiomatic way to share a DB pool or config). Signatures are inferred at compile time — no interface boilerplate, no dispatch cost.
Layered override points. A handler can define, in increasing order of control: dispatch (wrap every action — timing, auth, per-request context structs), notFound, uncaughtError, and finally handle, which bypasses httpz's router and dispatching entirely; JetZig hooks handle to supply its own routing2 — frameworks consume httpz as a raw connection/parsing engine, applications as a router.
Memory model. Each request gets req.arena / res.arena — a configurable thread-local buffer falling back to an std.heap.ArenaAllocator, freed after the response is written2. Response data must stay valid until after the action returns, so the arena (or res.writer()) is the intended path for dynamic bodies. Request bodies land in a static per-connection buffer, a large-buffer pool, or a dynamic allocation depending on size; request.lazy_read_size switches large bodies to streaming via req.reader(timeout_ms) instead of full buffering.
Batteries at the protocol level, not above it. Router with :params, middleware, query/form/multipart/JSON body parsing, server-sent events, internal metrics, a httpz.testing harness for unit-testing actions, and WebSocket upgrades (via the author's companion websocket.zig). No templating, ORM, or sessions — JetZig/Tokamak territory.
Production Notes
- No TLS. httpz speaks plaintext HTTP/1.1 only — no HTTP/2 or HTTP/3;
terminate TLS at a reverse proxy (nginx, Caddy).
- Zig version pinning is your main upgrade chore. Pin a
zig-X.YYbranch
in build.zig.zon. The 0.15 cycle changed the writer API (res.writer() takes a buffer — pass &.{}, httpz buffers internally)2; the 0.16 port rewires I/O through the new std.Io interface and is flagged experimental.
- Form parsing is off by default.
request.max_form_countand
request.max_multiform_count default to 0 — form/multipart parsing yields nothing until raised2. formData() and multiFormData() are mutually exclusive per request; multipart unescapes names in-place, subtly altering a later req.body().
- Lifetime discipline. Setting
res.bodyto memory that dies when your
action returns is the classic first bug; use res.arena.
- Single-maintainer risk. Karl Seguin is prolific and responsive (he also
maintains pg.zig and websocket.zig), but the bus factor is 1, with no corporate backing. Issue volume is low (~15 open): little churn, but a small contributor pool.
- The 140K req/s figure is author-reported on an M2 for a trivial handler2
— an upper bound, not a capacity plan.
When to Use / When Not
Use when:
- You are writing a Zig service and need an HTTP layer faster and more
defensive than std.http.Server.
- You want a library, not a framework — routing plus request/response
primitives, with your own architecture on top.
- You need WebSockets or SSE from the same server.
Avoid when:
- You need TLS in-process or HTTP/2+ — proxy in front, or use another stack.
- You want rails-style productivity in Zig — JetZig or Tokamak (both built on
httpz) give you more out of the box.
- Your team won't track Zig's breaking releases; a Go or Rust HTTP stack is
operationally far more stable.
Alternatives
- zigzap/zap — wraps the C library facil.io; mature C core, but not pure Zig.
Use it for a batteries-heavier microframework if the C dependency is fine.
- jetzig-framework/jetzig — full MVC-style framework built on httpz; use it
when you want templating, generators, and conventions rather than a library.
- cztomsik/tokamak — dependency-injection-oriented server framework on httpz;
use it for structured larger apps with DI ergonomics.
- ziglang/zig (
std.http.Server) — zero dependencies; fine for internal
tools and tests where throughput and hostile clients don't matter.
History
| Era | Date | Notes |
|---|---|---|
| Initial | 2023-03 | Repo created; HTTP/1.1 server independent of std.http.Server1. Branch-per-Zig-version model: master = latest stable Zig, dev = Zig master, zig-X.YY for older2. |
| Zig 0.14 support | 2025-03 | Tracks Zig 0.14.0 release. |
| Zig 0.15 support | 2025-08 | New std.Io.Writer interface; res.writer() gains a buffer parameter2. |
| Zig 0.16 port | 2026 | main(init: std.process.Init) / std.Io rework; author labels it experimental2. |
References
- ^ GitHub repository metadata — created 2023-03-13. https://github.com/karlseguin/http.zig
- ^ http.zig README (versions, alternatives, performance claim, handler/dispatch, memory & arenas, configuration). https://github.com/karlseguin/http.zig#readme
Tags
zig, http-server, http-1-1, web, networking, server-library, routing, websocket, server-sent-events, low-level