Low-allocation RFC 6455 websocket primitives for Zig with a specialized frame hot path, strict handshake validation, and permessage-deflate.
- 🧱 RFC 6455 core: frame parsing, masking, fragmentation, ping/pong, close handling, and server handshake validation.
- 🏎 Tight hot path:
Conn(comptime static)specializes role and policy at comptime to keep runtime branches out of the core path. - 📦 Low-allocation reads: stream frames chunk-by-chunk, read full frames, or borrow buffered payload slices when they fit.
- 🧠 Strict protocol checks: rejects malformed control frames, invalid close payloads, bad UTF-8, bad mask bits, and non-minimal extended lengths.
- 🗜
permessage-deflate: handshake negotiation plus compressed message read/write support, withserver_no_context_takeoverandclient_no_context_takeover. - 🧠 Optional context takeover: compile-time
Conn.Typetoggle (permessage_deflate_context_takeover) enables cross-message compression state when negotiated. - 🎛 Per-message compression policy: compile-time
Conn.Typeknobs decide when messages are compressed (permessage_deflate_min_payload_len,permessage_deflate_require_compression_gain). - ⏱ Timeout hooks: optional read, write, and flush time budgets with typed runtime hooks for framework-owned transports.
- 🔁 Convenience helpers:
readMessage,writeText,writeBinary,writePing,writePong, andwriteClose. - 🧩 Typed message handlers:
Handler.run(...)with typed user state, sync/async execution modes, and response coercion from[]const u8,[][]const u8, or structs withbody. - 🧪 Validation stack: unit tests, fuzz/property tests, a cross-library interop matrix, soak runners, and benchmarks live alongside the library.
After you have already accepted the websocket upgrade and have a reader/writer pair:
const std = @import("std");
const zws = @import("zws");
fn runEcho(reader: *std.Io.Reader, writer: *std.Io.Writer) !void {
var conn = zws.Conn.Server.init(reader, writer, .{});
var message_buf: [4096]u8 = undefined;
while (true) {
const message = conn.readMessage(message_buf[0..]) catch |err| switch (err) {
error.ConnectionClosed => break,
else => |e| return e,
};
switch (message.opcode) {
.text => try conn.writeText(message.payload),
.binary => try conn.writeBinary(message.payload),
}
try conn.flush();
}
}For explicit handshake validation on a raw stream:
const negotiated = try zws.Handshake.upgrade(reader, writer);
_ = negotiated;
try writer.flush();For a full standalone echo server example:
zig build examples -Dexample=echo-server -- --port=9001 --compressionFor a frame-oriented echo server that stays on the low-level frame APIs:
zig build examples -Dexample=frame-echo-server -- --port=9002For a simple websocket client that performs the HTTP upgrade and then uses zws.Conn.Client:
zig build examples -Dexample=client -- --host=127.0.0.1 --port=9001 --message=helloFor a typed per-message handler loop with user-owned state:
const io = std.Io.Threaded.global_single_threaded.io();
var app_state = AppState{};
var scratch = zws.Handler.Scratch(.{
.receive_mode = .solid_slice,
}).init();
fn onMessage(ctx: *zws.Handler.SliceContext(.{
.receive_mode = .solid_slice,
}, zws.Conn.Server, AppState)) ![]const u8 {
ctx.state.seen += 1;
return ctx.message.payload;
}
try zws.Handler.run(.{
.receive_mode = .solid_slice,
}, io, &conn, &app_state, &scratch, onMessage);Add as a dependency:
zig fetch --save <git-or-tarball-url>build.zig:
const zws_dep = b.dependency("zws", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zws", zws_dep.module("zws"));zws.Conn.Type(.{ ... })creates a websocket connection type specialized for a fixed role and policy set.zws.Conn.Default,zws.Conn.Server, andzws.Conn.Clientare the common aliases.- Low-level read path:
beginFrame,readFrameChunk,readFrameAll,discardFrame,readFrameBorrowed. - Convenience read path:
readFrame,readMessage,readMessageBorrowed. - Write path:
writeFrame,writeText,writeBinary,writePing,writePong,writeClose,flush. - Handshake path:
Handshake.computeAcceptKey,Handshake.upgrade. - Compression path:
Extensions.PerMessageDeflate,Conn.PerMessageDeflateConfig,Conn.Config.permessage_deflate. - Runtime hooks:
Observe.TimeoutConfig,Observe.DefaultRuntimeHooks,Conn.TypeWithHooks(...). - Message handler loop:
Handler.run(...),Handler.Options,Handler.SliceContext(...),Handler.StreamContext(...),Handler.Response.
DOCUMENTATION.md: API stability, transport/runtime expectations, deployment notes, and validation entry points.
src/root.zig: public package surfacesrc/conn.zig: connection state machine and frame I/Osrc/handshake.zig: server upgrade parsing, validation, and101response writingsrc/extensions.zig: extension negotiation helpersbenchmark/bench.zig: benchmark clientbenchmark/zws_server.zig: standalone benchmark serverexamples/echo_server.zig: standalone echo server exampleexamples/frame_echo_server.zig: frame-level echo server example usingreadFrameBorrowedexamples/ws_client.zig: standalone client example with manual HTTP upgradevalidation/: Zig interop and soak runners, plus peer dependency metadata
Benchmark support lives under benchmark/.
zig build bench-compare -Doptimize=ReleaseFastEnvironment overrides:
SINGLE_CONNS=1 MULTI_CONNS=16 ITERS=200000 WARMUP=10000 MSG_SIZE=16 zig build bench-compare -Doptimize=ReleaseFast
SINGLE_CONNS=1 MULTI_CONNS=16 ITERS=200000 WARMUP=10000 PIPELINE_DEPTH=8 MSG_SIZE=16 zig build bench-compare -Doptimize=ReleaseFast
ROUNDS=2 BENCH_TIMEOUT_MS=120000 ZWS_DEADLINE_MS=30000 UWS_DEADLINE_MS=30000 zig build bench-compare -Doptimize=ReleaseFastSource: benchmark/results/latest.json
Config: host=127.0.0.1 path=/ rounds=2 single_conns=1 multi_conns=16 iters=200000 warmup=10000 pipeline_depth=8 msg_size=16 bench_timeout_ms=120000 zws_deadline_ms=30000 uws_deadline_ms=30000
| Suite | sync | sync+dl | async | async+dl |
|---|---|---|---|---|
| single / non-pipelined | +5.16% | -3.88% | +12.51% | +18.04% |
| single / pipelined | -4.63% | -17.46% | +182.40% | +139.30% |
| multi / non-pipelined | +106.84% | +110.13% | +120.85% | +123.50% |
| multi / pipelined | +108.88% | +77.87% | +1007.78% | +924.60% |
Values show zws vs matching uWS throughput delta.
Fairness notes: all peers use the same benchmark client, identical per-suite client settings, and the matrix runs strict interleaved rounds for every peer inside each suite.
For benchmark details, see benchmark/README.md.
zig build test
zig build bench -- --conns=1 --iters=2000 --warmup=100
zig build interop
zig build soak
zig build validate
zig build examples
zig build bench-compare -Doptimize=ReleaseFastzws is intentionally focused on a small websocket core.
- Server-side RFC 6455 handshake validation is included.
- Connection state is synchronous and stream-oriented.
permessage-deflateis implemented and negotiated when enabled.- Compression is disabled by default (
Config.permessage_deflate = null). - Even when negotiated, outgoing compression is opt-in (
Conn.PerMessageDeflateConfig.compress_outgoing = falseby default). - Context takeover support is disabled by default (
Conn.StaticConfig.permessage_deflate_context_takeover = false). - When enabled,
Conn.StaticConfigdefaults (permessage_deflate_min_payload_len = 64,permessage_deflate_require_compression_gain = true) skip tiny messages and avoid non-beneficial compression. - No TLS or HTTP server framework is bundled; use the raw stream API or the example server as the integration point.
permessage-deflateframing is implemented withstd.compress.flate(both non-takeover and optional context-takeover paths).