Quick start
quicz is a QUIC / HTTP/3 implementation in pure Zig
(40/41 features, 1820 tests, three-implementation interop verified). The
recommended production API is the async std.Io runtime (quicz.runtime):
an event-driven server with per-connection handlers and an async client. This
guide walks through the HTTP/3 path (recommended for most apps) and the
low-level stream-echo path (for custom protocols). Public APIs may still evolve.
Requirements
Section titled “Requirements”- Zig 0.16.0 — the library is pure Zig, no C dependencies.
Add the dependency
Section titled “Add the dependency”zig fetch --save git+https://github.com/venjiang/quiczIn build.zig:
const quicz_dep = b.dependency("quicz", .{ .target = target, .optimize = optimize });exe.root_module.addImport("quicz", quicz_dep.module("quicz"));Then const quicz = @import("quicz");.
Common setup: the event loop
Section titled “Common setup: the event loop”Both server and client run on a Zig std.Io instance. The Threaded backend
executes async I/O on a thread pool; your app drives streams from async tasks.
var threaded = std.Io.Threaded.init(allocator, .{});defer threaded.deinit();const io = threaded.io();HTTP/3 server
Section titled “HTTP/3 server”runtime.Server owns the socket, endpoint, and connection lifecycle. Use
serveH3 with a synchronous request handler.
const Server = quicz.runtime.server.Server;
fn handleRequest(req: quicz.h3_request.DecodedRequest) quicz.h3_request.Response { if (std.mem.eql(u8, req.path, "/")) { return .{ .status = 200, .body = "Hello from quicz HTTP/3!" }; } // Streamed (chunked) response body — sent as multiple DATA frames. if (std.mem.eql(u8, req.path, "/stream")) { return .{ .status = 200, .body_stream = quicz.h3_request.ResponseBody.fromRepeating(allocator, 'S', 65536) catch unreachable, }; } return .{ .status = 404, .body = "not found" };}
var server = try Server.init(allocator, io, .{ .port = 4433, .alpn = &.{"h3"}, .cert_der = &certificate_der, // DER certificate .private_key = &server_private_key, // matching private key});defer server.deinit();try server.serveH3(.{}, handleRequest); // options: qpack_max_table_capacity, qpack_blocked_streams
// Block until killed (serveLoop runs as a concurrent task).server.drive_group.await(io) catch {};Test with curl --http3-prior https://127.0.0.1:4433/ -k -v (a curl build with
HTTP/3 support).
Response variants
Section titled “Response variants”| Field | Meaning |
|---|---|
.body = slice |
Single contiguous body, encoded as one DATA frame |
.body_stream = ResponseBody |
Chunked body (takes precedence over body) |
| neither | Bodyless response (HEADERS + fin) |
Request bodies are aggregated up to max_request_body_size (1 MiB default);
inside the handler req.body holds the full body (or null). Oversized bodies
are rejected with 413 + STOP_SENDING.
HTTP/3 client
Section titled “HTTP/3 client”const Client = quicz.runtime.client.Client;const H3Client = quicz.runtime.h3_client.H3Client;
var client = try Client.init(allocator, io, .{ .server_port = 4433, .server_name = "localhost", .alpn = &.{"h3"}, .insecure_skip_verify = true, // null ca_bundle also skips verification});defer client.deinit();try client.connect();
var h3cli = H3Client.init(allocator, &client, 4096, 8); // qpack cap, blocked streamsdefer h3cli.deinit();try h3cli.run(); // waits for the server SETTINGS
// Send a GET request.const stream = try h3cli.sendRequest(.{ .method = "GET", .path = "/", .authority = "localhost",});const resp = try h3cli.receiveResponse(stream);if (resp.isSuccess()) { // resp.body is the aggregated response body (or null).}client.close();Streamed request body
Section titled “Streamed request body”For large uploads, sendRequestStreamed sends the body as bounded DATA frames,
blocking until fully drained (flow-control credit is awaited):
const body = try quicz.h3_request.ResponseBody.fromRepeating(allocator, 'A', 20 * 1024);const stream = try h3cli.sendRequestStreamed(.{ .method = "POST", .path = "/echo", .authority = "localhost",}, body);const resp = try h3cli.receiveResponse(stream);Low-level stream echo (custom protocols)
Section titled “Low-level stream echo (custom protocols)”For non-HTTP protocols, use Server.serve with a per-connection handler
(std.http model). Each connection gets its own handler task.
const ServerConnection = quicz.runtime.server.ServerConnection;
fn echoHandler(conn: ServerConnection) std.Io.Cancelable!void { var c = conn; var stream = c.acceptStream() catch return; var buf: [65536]u8 = undefined; while (true) { const n = stream.receive(&buf) catch return; if (n == 0) break; // EOF stream.send(buf[0..n], false) catch return; }}
var server = try Server.init(allocator, io, .{ .port = 4433, .alpn = &.{"hq-interop"}, .cert_der = &certificate_der, .private_key = &server_private_key,});defer server.deinit();try server.serve(&echoHandler);Client side: connect() then send / receive on a stream:
try client.connect();const sid = try client.send("hello", false);var buf: [4096]u8 = undefined;const n = try client.receive(sid, &buf); // 0 = EOFCertificates
Section titled “Certificates”The examples bundle a local test-only P-256 key pair. For production:
- macOS / arm64: ECDSA P-256 certificates work.
- Linux x86_64: Zig 0.16’s
std.cryptohas a known codegen bug for P-256/P-384/Ed25519 signature verification. Use RSA certificates and a Release build (-Doptimize=ReleaseFast). An OpenSSL-generated RSA certificate verifies correctly on Linux.
Server.Config supports bind_addr (default 127.0.0.1); set it to
.{0,0,0,0} to listen on all interfaces.
Common patterns
Section titled “Common patterns”- Per-connection handler task —
Server.serve/serveH3spawn one task per connection; each connection’s resources are single-owner (no refcounting). - Non-blocking multistream — poll with
tryAcceptStreamId/tryReceiveStreamData/connStreamIdsand park onwaitStreamActivityinstead of blocking on one stream. - Concurrency —
std.Io.Group.concurrentruns independent client/server tasks;examples/multi_client_bench.zigshows N concurrent clients.
Build and run the probes
Section titled “Build and run the probes”zig build # build the libraryzig build test --summary all # 1820 unit testszig build run-tls13-udp-loopback # TLS 1.3 UDP loopbackzig build run-interop-client-standalone # interop self-testzig build run-quic-bench # throughput / latency benchmarkszig fmt --check build.zig src examples # format checkRunnable demos (echo, DATAGRAM, post-quantum, 0-RTT, H3 server, congestion, connection migration) on the examples page; benchmark numbers on the performance page.
Interop testing
Section titled “Interop testing”quicz passes a full bidirectional interop matrix (7/7) against quic-go, quiche, s2n-quic, and quinn — certificate-verified TLS 1.3 with a proper CA chain.
| Direction | Peer | Result |
|---|---|---|
| Forward (quicz client → server) | quic-go / quiche / s2n-quic | echo_bytes=19, cert verified |
| Reverse (client → quicz server) | quic-go / quinn / quiche / s2n-quic | echo_streams=2, echo_bytes=10 |
zig build && zig-out/bin/quicz-interop-runtime-server 4433 cert.pem key.pemzig-out/bin/quicz-interop-runtime-client 127.0.0.1 4433 quicz-echo-ca.pem localhostexamples/interop/run_reverse_interop.sh all 4433Security
Section titled “Security”THREAT_MODEL.md
documents the trust boundary and defenses against in-scope attacks, each with
code and test references. See the threat model page.
Development map
Section titled “Development map”| Need | Start here |
|---|---|
| Async I/O runtime | src/runtime/ |
| Runtime API reference | /api/ |
| Connection state machine | src/quic/connection.zig |
| Pure-Zig TLS 1.3 | src/tls/tls13.zig |
| HTTP/3 / QPACK / WebTransport | src/h3/ |
| Runnable examples | examples/ |
| Protocol status & evidence | status |
License
Section titled “License”MIT.