← /projects

Zig Server —
Custom HTTP/1.1

[ 2026 ] [ Zig · TCP · Linux epoll ] [ Zero dependencies ]
Zig TCP Linux epoll HTTP/1.1 Systems Networking

A zero-dependency HTTP/1.1 web server written from scratch in Zig. Raw TCP socket listener, hand-rolled HTTP protocol parser, and Linux epoll for non-blocking async I/O. No libuv, no framework, no runtime. The simplest thing that could possibly work at scale.

This site is served by a custom HTTP/1.1 server written in Zig, using epoll and POSIX I/O directly. No libuv, no framework. The simplest thing that could possibly work at scale.

Architecture

TCP Layer

The server opens a raw POSIX socket with socket(AF_INET, SOCK_STREAM, 0), sets SO_REUSEADDR, binds, and listens. New connections are accepted in a loop and registered with the epoll event loop for read-readiness notifications.

// Accept loop (Zig)
while (true) {
    const events = try epoll.wait(&event_buf, -1);
    for (events) |ev| {
        if (ev.data.fd == server_fd) {
            const client = try accept(server_fd);
            try epoll.add(client, .{ .events = EPOLLIN | EPOLLET });
        } else {
            try handleClient(ev.data.fd);
        }
    }
}

HTTP Parser

The parser is a hand-written state machine that reads bytes directly from the socket buffer. It identifies the request line, extracts the method, path, and HTTP version, then scans headers until the double CRLF. No regex, no allocations beyond the receive buffer.

epoll & Non-Blocking I/O

All sockets are set to O_NONBLOCK. The epoll instance watches for EPOLLIN events in edge-triggered mode (EPOLLET). Edge-triggered means the kernel notifies once per state change rather than continuously while data is available — this requires reading until EAGAIN to drain the socket fully, but eliminates redundant wakeups on busy connections.

Why From Scratch

The goal wasn't to build a production web server. It was to understand exactly what happens between a client sending bytes and a server returning a response — without any of it being abstracted away. Writing the parser and the event loop by hand is the only way to actually understand what libuv or Nginx are doing for you.

Zig is a good fit for this because it exposes POSIX syscalls directly through std.os, has no hidden allocations, and makes the control flow explicit. Every call that can fail must be handled explicitly.


Read the companion post: Building an HTTP/1.1 Server in Zig Without libuv →