TECHNOLOGY LAB · SYSTEMS ENGINEERING

A modern successor to Zap

Familiar to write.
Explicit by design.

Meet Baz — Bounded Async Zap. A pure Zig web framework on bounded/http. Typed applications, streaming responses, and memory boundaries you can reason about.

Its foundation, bounded/http, takes inspiration from TigerStyle: set explicit limits, reserve resources up front, and check ownership invariants. Connections, buffers, queues, and workers have fixed capacities; request sizes, response sizes, and I/O deadlines have enforced limits. When capacity runs out, the engine applies backpressure or rejects work instead of growing without bounds. These guarantees cover the framework’s resources; application code still owns its memory use and must cooperate with cancellation.

Linux
io_uring
macOS
kqueue
Windows x64
IOCP

Native HTTP I/O powered by bounded/http.

PURE ZIG · EXACT 0.16.0CALLER-SUPPLIED std.IoMIT LICENSE
Typed.

App state and endpoint methods.
Ordinary Zig composition.

Explicit.

Borrowed bytes in.
Decoding when you ask.

Bounded.

Framework capacity at startup.
Output reserved before dispatch.

01 / The idea

The good parts of Zap.
A fresh foundation.

Zap made building Zig web applications approachable. Baz carries forward its App and endpoint ergonomics, with a new API and a separate, bounded HTTP engine underneath.

The framework and its HTTP engine are written in Zig. Baz replaces Zap’s facil.io C foundation with bounded/http.

Native Windows support, too. Baz runs on Linux, macOS, and Windows x64—an addition to Zap’s platform support. Read the native evidence →

01 — COMPOSITION

Your types, your App.

Use App(Shared), ordinary structs, and one router. Each App owns its lifecycle; your shared state remains explicit.

02 — REQUEST DATA

Keep the original bytes.

001 is text. So is false. Duplicate fields keep their order, and files[] is simply a name.

03 — OWNERSHIP

Know what stays alive.

Read borrowed input during the callback. Decode into your own buffer. Build a private response in capacity reserved before your handler runs.

Baz is a new framework, with its own package and API. The migration guide maps familiar Zap concepts to their Baz equivalents.

02 / An ordinary Zig App

A struct. A method.
A clear place for everything.

Define your shared state, choose App(Shared), and register an endpoint. The context gives you the request, your state, route captures, and a bounded response draft.

const Hello = struct {
    pub fn get(_: *Hello, ctx: *Context) !void {
        const query = try ctx.request.query();
        if (query.firstRaw("name")) |param| {
            var decoded: [1024]u8 = undefined;
            const name = try web.params.percentDecodeInto(param.value_raw, &decoded);
            return ctx.response.text(200, name); // copies before stack storage ends
        }
        return ctx.response.text(200, ctx.shared.greeting);
    }
};

This excerpt uses Context = web.App(Shared).Context. Register it with try app.endpoint("/hello", &hello). See the complete App example for initialization, stable state, and shutdown.

Start with explicit state.

Shared state and endpoint instances stay at stable addresses through App destruction. Register routes before startup. Synchronize mutable state used by concurrent callbacks.

Return a finished response.

Text, bytes, copied headers, JSON, and standard formatting all use bounded output. A failed handler discards its private draft before error mapping.

OWNERSHIPThis name is temporary, so response.text copies it. For retained input and immutable startup assets, borrowBody borrows the payload independently of staging capacity, within the total response limit. See the copy paths and limits → See a complete file-response handler →

Send the first bytes. Keep working.

Write a little. Flush it to the client. Pause, then send the next update—all inside the same handler.

fn progress(ctx: *Application.Context) !void {
    try ctx.response.header("Cache-Control", "no-cache");
    var stream = try ctx.response.stream(200, "text/plain; charset=utf-8", .{});
    const out = stream.writer();
    try out.writeAll("Starting…\n");
    try out.flush();
    try ctx.sleep(.fromMilliseconds(500));
    try out.print("Completed step {d} of {d}\n", .{ 1, 2 });
    try out.flush();
    try ctx.sleep(.fromMilliseconds(500));
    try out.writeAll("Done.\n");
    try stream.finish();
}

STREAMINGout is a standard std.Io.Writer. Each flush() sends the current bytes before the handler continues.

Updates as they happen.

Progress reports, generated text, and incremental output can reach clients before the handler returns. Use curl -N to watch the example arrive in three parts.

Bounded while you wait.

A stream uses one existing application worker and fixed output storage. A slow reader applies backpressure; cancellation releases the waiting handler safely. The HTTP I/O loop keeps running.

The complete example selects fixed workers. Headers are final after the first flush, and response size and request time remain bounded. Read the streaming guide → Native streaming evidence →

Many waiting streams. A small worker pool.

For higher concurrency, typed continuations return flush, wait, or finish. Waiting releases the executor while bounded typed state stays alive. Middleware runs once; completion, timeout, and disconnect clean up that state. Works inline or with fixed workers. Read the continuation guide →

Current limit: stream writes copy bytes. You cannot insert a borrowBody() call between flushes. See the response combination rules →

Send a file. Skip the body copy.

Already have a large image or file in memory? borrowBody() lets Baz send those existing bytes directly, avoiding a copy into its response buffer. A 5 MiB file stays in its original storage instead of being copied into another 5 MiB framework buffer.

This complete handler shows the API with an embedded HTML file: GET / returns the file’s contents as the entire response body. Use the same call with your image bytes and content type. Small bodies below the default 256-byte threshold—including this tiny HTML file—may still be copied.

const Shared = struct {};
const Application = web.App(Shared);

fn index(ctx: *Application.Context) !void {
    return ctx.response.borrowBody(200, "text/html; charset=utf-8", @embedFile("assets/serve_index.html"));
}

Here web = @import("baz"). The complete example registers try app.route("GET", "/", index). @embedFile includes the file at compile time, so this handler performs no filesystem I/O. Run it with zig build run-serve -Doptimize=ReleaseSafe -- --port 8080.

FEWER COPIESresponse.bytes() copies your body into bounded staging. borrowBody() avoids that payload copy for large retained bodies; a large file can use a small output arena. Headers and normal kernel transfers still have copying costs. See exactly where copies happen →

borrowBody() prepares one complete body. When this handler returns successfully, App publishes status 200, HTTP headers, and the file bytes. There is no body prefix or suffix. The embedded bytes stay alive for transmission.

Headers and handler work still fit.

Authenticate, choose an asset, and add response headers in your handler. Headers can be added before or after borrowBody() while the response is unpublished. A large immutable file can use a small output arena; the total response limit still applies.

Streaming + borrowing: not yet.

Writing and flushing a prefix, calling borrowBody() with an image, then continuing the stream is currently unsupported. Borrowing the whole body and starting a stream are separate choices—even before the first flush. Use the stream writer’s writeAll(image) to put image bytes between other streamed chunks; that path copies through bounded staging.

Verified on native Linux, macOS, and Windows: the separate 5 MiB fixture sends the original payload with a 4 KiB arena and no framework payload copy. Read the copy-counter and ownership evidence → Read what can be combined →

From a background job to a live page.

The complete jobs example puts Mustache, cookie sessions and SSE together. A startup producer publishes progress; bounded notifications wake waiting streams while their workers remain available. Event IDs support explicit, finite replay, and each resumed callback checks the session again. Run the application → See the job studio → Read the SSE and notification API →

03 / Data stays data

No guessing what
your parameters mean.

Query parameters and URL-encoded forms are ordered lists of raw text slices. Decode when needed, into a buffer you supply. Repeated fields remain repeated fields.

Raw input, ordered views, explicit decodingThe query id=001 and two tag[] fields remain three ordered pairs borrowing the same input. The value a+b stays unchanged in the raw view; percent decoding keeps plus, while form decoding changes it to a space. RETAINED QUERY BUFFERid=001&tag[]=a+b&tag[]=c%2Bd PAIR 01PAIR 02PAIR 03 id → 001tag[] → a+btag[] → c%2Bd Leading zeros stay.Brackets stay in the name.Duplicates stay in order. percentDecodeInto: a+b → a+bformDecodeInto: a+b → a b BOTH DECODERS WRITE INTO CALLER-SUPPLIED STORAGE
A raw query keeps spelling and ordering. The selected decoder determines how + is treated.
InputRaw valueExplicit operationResult
id=001001Read the sliceText, including leading zeros
enabled=falsefalseRead the sliceText; no boolean conversion
q=a+b%2Bca+b%2BcpercentDecodeIntoa+b+c
q=a+b%2Bca+b%2BcformDecodeIntoa b+c

One multipart iterator.

Fields and files share the same flat representation: a raw name, optional filename and content type, and borrowed bytes. One file or several files use the same loop. Baz never saves an upload implicitly.

Read the upload example →

Copies have a place.

Multipart parsing validates the complete supported body before iteration. If HTTP chunks split a body, request a bounded copy into caller storage before parsing a contiguous form. Application access to borrowed views ends with the callback.

Read the ownership contract →

04 / Built on bounded/http

Application ergonomics above.
HTTP ownership underneath.

Baz imports bounded_http as a pinned external dependency. The engine stays independently usable. Baz supplies the application layer: routing, request views, endpoint composition, and responses.

Baz and bounded/http are separate packagesYour application supplies shared state, endpoint instances, and a caller I/O capability. Baz provides the typed App API. bounded/http supplies parsing, scheduling, response reservations, and Linux io_uring, macOS kqueue, or Windows IOCP transport. Finite blocking services use explicit fixed application workers. Your applicationShared state · endpoint structs · explicit application scratch Caller’s std.IoStartup + application services BazApp(Shared) · router · request views · response drafts Explicit fixed workersFor finite blocking services.Application code and providerown their resource behavior. NO AUTOMATIC CANCELLATION bounded/httpHTTP parser · scheduler · reservations · connection ownership Linux · io_uringmacOS · kqueueWindows · IOCP CURRENT VERIFIED BAZ BASELINE
Two packages, one explicit boundary. Baz uses the engine’s public embedding API; transport and scheduler changes belong upstream.

Use Zig’s std.Io today.

Pass your caller’s std.Io at initialization. Standard memory readers and writers work inline. Finite blocking services require explicit fixed workers; the provider’s resource use remains the application’s responsibility.

Keep the engine’s transports.

Baz supports Linux with io_uring, macOS with kqueue, and native Windows x64 with IOCP. HTTP transport uses the engine directly. A Baz-owned std.Io implementation is a later design step.

A response reservation comes before handler side effectsRetain validated input, reserve the full draft capacity, run the handler once, then finalize and send. Input and output storage remain retained until all application and kernel borrows have returned. 01 / INPUT02 / RESERVATION04 / OUTPUT 03 / APPLICATION Retain the requestSecure capacityRun the handlerFinalize & send Validated, borrowed bytesDrain older output if neededRetain pending borrows One unpublished draft REUSE ONLY WHEN ALL APPLICATION AND KERNEL BORROWS HAVE RETURNED
Output pressure can delay dispatch. It does not replay your one-shot handler. Storage is reusable only after all application and kernel borrows return.

05 / Get started

A working App in one command.

Use exact Zig 0.16.0 and Python 3. Clone Baz, then start the demo. The pinned HTTP engine is fetched automatically.

01 · Start the App
git clone https://github.com/technologylab-ai/baz.git
cd baz
zig build run-app -Doptimize=ReleaseSafe -- --port 8080
02 · In another terminal, from the Baz directory
curl 'http://127.0.0.1:8080/hello?name=Hello%20Zig'
curl --data 'value=x+y%2Bz' http://127.0.0.1:8080/form
curl -F 'files[]=@.zig-version' http://127.0.0.1:8080/upload

The current executable serves IPv4 loopback over plain HTTP/1.1. Stop it with Ctrl-C. On Windows, use curl.exe; executables have an .exe suffix. For embedding Baz in your own project, follow the package guide and independent consumer fixture.

06 / Explore the examples

Examples you can run.

Twenty Zap ports and a new streaming example, using Baz’s public API. Every card opens maintained source in the reader. Pick a small example, then follow its types.

Build real pages with Mustache templates: a greeting form, typed user cards, and reusable partials. Parse once at startup and render straight into reserved HTML storage. The complete Zig example is just 52 lines → See the rendered preview →

24 of 24 examples

hello

A minimal HTML response and an explicit route.

hello2

Inspect methods, raw queries, headers, and bounded bodies.

hello_json

JSON user lookup, with explicit ID parsing.

simple_router

Functions, stateful routes, and a synchronized counter.

routes

Static and dynamic responses in one router.

serve

Serve immutable embedded assets through explicit routes.

sendfile

File content as an embedded asset; no sendfile syscall.

senderror

Controlled errors, with no client-visible stack trace.

accept

Explicit, bounded content negotiation.

mustache

A greeting form and user cards: startup templates, typed data, bounded HTML.

continuations

Many waiting streams, a small worker pool: typed flush, wait, and finish callbacks.

streaming

Write, flush, sleep, and write again through a standard Zig writer.

jobs

A complete application: Mustache, cookie sessions and live job progress over bounded SSE.

app_basic

Typed Shared, endpoint state, and instance shutdown.

app_errors

Error mapping and discarded private response drafts.

endpoint

Bounded user CRUD on explicit application workers.

app_auth

Public authentication middleware and typed request locals.

endpoint_auth

Stateful endpoints with an explicit authentication check.

middleware

Public ordered before/after/cleanup hooks and typed request locals.

middleware_with_endpoint

Endpoint composition with an early-stop path.

userpass_session

32 reusable sessions, fixed server expiry, and logout across devices.

cookies

Borrowed cookie input, explicit expiry, and validated Set-Cookie output.

http_params

Raw duplicates and explicit query versus form decoding.

bindataformpost

One flat loop for fields and files, with bounded previews.

Build all examples, or run one
zig build examples -Doptimize=ReleaseSafe
zig build run-http_params -Doptimize=ReleaseSafe -- --port 8080

Cookies and redirects are public APIs, with explicit expiry and a local login/logout example. Middleware, authentication policy and session storage remain explicit application code. Read the complete catalog and its deliberate adaptations →

07 / Measured results

A first comparison.
The conditions included.

A basic same-host loopback comparison with Zap, measured on 6 September 2026. These results describe the initial Baz prototype c152e59, before package extraction.

Baz prototype and Zap, 32 connections Median requests per second: Mac Baz 254,261 and Zap 245,054; Linux Baz 363,594 and Zap 205,310. The table includes the one-connection results as well. 0 100k 200k 300k 400k macOS M3 Max 254,261 245,054 Linux Core Ultra 7 363,594 205,310
Median requests per second at 32 connections / 2 client threads. Bar lengths share a zero baseline. Compare implementations within each host.
HostConn. / threadsBaz req/sZap req/sBaz / Zap
macOS · M3 Max32 / 2254,261245,0541.038×
Linux · Core Ultra 732 / 2363,594205,3101.771×
macOS · M3 Max1 / 133,19952,8090.629×
Linux · Core Ultra 71 / 174,38672,5751.025×

macOS: Apple M3 Max. Linux: Intel Core Ultra 7 258V. Three paired trials per profile; one-second warmup and three-second measurements. ReleaseSafe with Zig assertions enabled, one server thread, 128 connection slots, a 13-byte response, keep-alive, no pipelining, and no CPU affinity.

The Mac’s one-connection result is lower than Zap’s. These short trials establish neither production capacity nor latency guarantees, and do not isolate Baz’s API overhead. The hosts used different wrk revisions; Zap retained its inherited C optimization and sanitizer settings.

All 24 measured trials completed with zero wrk-reported socket or non-2xx/3xx errors. Exact responses were checked before and after timing, rather than for every timed response. The report preserves ranges, versions, commands, raw results, and cleanup evidence.

Read the experiment and raw evidence

08 / What comes next

A useful first slice.
A visible roadmap.

Baz supports native Windows x64, Linux, and macOS. All three have passed Debug/ReleaseSafe verification, package checks, all 20 ported-example groups, and all 14 streaming groups. Windows also passed shard-handoff and console-shutdown cases. Baz remains experimental; these are correctness gates, not production qualification.

Implemented

The streaming App.

Next API work

More ways to compose.

  • Further response-copy reduction with explicit stream borrows
  • Broader asynchronous service integration

Deferred / out of scope

Keep the scope honest.

  • WebSockets: needs an upgrade lifecycle
  • An owned std.Io provider and prototype: deferred
  • TLS: out of scope

The framework reserves its own storage at startup. Application code and the caller’s I/O provider have separate resource responsibilities. Fixed workers do not provide arbitrary application isolation.

Follow the multi-session roadmap →

09 / Read further

From first route
to the ownership details.

Guides stay as Markdown in the repository. The browser reader adds a table of contents, highlighted source, raw-file access, and print support.