# Dusty - HTTP server/client library

**URL:** <https://ziggit.dev/t/dusty-http-server-client-library/12807>\
**Category:** Showcase\
**Tags:** networking, io, http, llm, async\
**Created:** [November 2, 2025, 10:47am UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807 "2025-11-02T10:47:35Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [November 2, 2025, 10:47am UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/1 "2025-11-02T10:47:35Z")

</div>

As if Zig didn’t already have enough HTTP servers, I’m creating another one. This is very early, but it’s one of the reasons for me working on [zio](https://github.com/lalinsky/zio) in the first place. I have a server application, where I want to run custom TCP protocol, HTTP server and NATS servers, all in a single event loop, all single-threaded for the networking part.

So let me introduce Dusty, a simple HTTP server, based on my zio runtime for networking and llhttp for request parsing. Feel free to experiment with it. Later on, I’ll probably make the code use `std.Io` instead of zio directly, but for now zio is a hard dependency.

> **[GitHub - lalinsky/dusty: HTTP client/server library for Zig](https://github.com/lalinsky/dusty)**
>
> HTTP client/server library for Zig

---

<div class="post-metadata">

**Author:** ![vic-Rokx](https://ziggit.dev/user_avatar/ziggit.dev/vic-rokx/32/4167_2.png) [@vic-Rokx](https://ziggit.dev/u/vic-Rokx)\
**Post date:** [November 3, 2025, 7:34pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/2 "2025-11-03T19:34:04Z")

</div>

Hey! So I cloned your repo and been doing some basic testing, curious, what the bottleneck is here, both servers are running in debug mode, personally I can’t ZIO to work on my laptop, but dusty works fine? Is llhttp the bottleneck here, cause the metrics seems to be extremely off, and I also have built my own custom event loop, which is single threaded?

Its a very simple wrk request just to see the event loops ability  
wrk -t1 -c50 -d10s [http://127.0.0.1:8080/ping](http://127.0.0.1:8080/ping)

dusty results

```zig
Running 10s test @ http://127.0.0.1:8080/ping
  1 threads and 50 connections
  Thread Stats Avg Stdev Max +/- Stdev
    Latency 421.05us 100.35us 11.41ms 96.89%
    Req/Sec 111.50k 2.28k 113.55k 96.04%
  1120759 requests in 10.10s, 49.17MB read
Requests/sec: 110958.17
Transfer/sec: 4.87MB

```

other server results

```zig
Running 10s test @ http://127.0.0.1:8080/ping
  1 threads and 50 connections
  Thread Stats Avg Stdev Max +/- Stdev
    Latency 185.82us 90.20us 2.57ms 89.17%
    Req/Sec 167.04k 25.10k 188.65k 86.14%
  1678688 requests in 10.10s, 158.49MB read
Requests/sec: 166170.75
Transfer/sec: 15.69MB

```

For dusty I’m just using the basic example setup but with the following handle root

```zig
fn handleRoot(ctx: *AppContext, req: *http.Request, res: *http.Response) !void {
    _ = ctx;
    _ = req;
    res.body = "SUCCESS\n";
}

```

```zig
pub fn runServer(allocator: std.mem.Allocator, rt: *zio.Runtime) !void {
    var ctx: AppContext = .{ .rt = rt };
    const AppServer = http.Server(AppContext);

    const config: http.ServerConfig = .{
        .timeout = .{
            .request = 60 * std.time.ms_per_s,
            .keepalive = 300 * std.time.ms_per_s,
        },
    };

    var server = AppServer.init(allocator, config, &ctx);
    defer server.deinit();

    // Register routes
    server.router.get("/ping", handleRoot);
    var signal = try zio.Signal.init(.interrupt);
    defer signal.deinit();

    const addr = try zio.net.IpAddress.parseIp("127.0.0.1", 8080);

    var task = try rt.spawn(AppServer.listen, .{ &server, rt, addr }, .{});
    defer task.cancel(rt);

    while (true) {
        const result = try zio.select(rt, .{ .task = &task, .signal = &signal });
        switch (result) {
            .task => |r| {
                return r;
            },
            .signal => {
                server.stop();
            },
        }
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var rt = try zio.Runtime.init(allocator, .{});
    defer rt.deinit();

    try rt.runUntilComplete(runServer, .{ allocator, rt }, .{});
}

```

Love the work so far, but not sure if there is some configuration or something I need to do to get Dusty up in performance, or if you are mainly working on ergonomics, but I assume zio is great, since you mentioned it outperfs go and tokio.

Thanks for all the help!

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [November 3, 2025, 7:48pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/3 "2025-11-03T19:48:46Z")

</div>

> [@vic-Rokx](#):
>
> both servers are running in debug mode,

This might be the problem, try running them in ReleaseSafe, there is a lot of code, specially in the C parser, that would really benefit from optimizations. I haven’t spend much time benchmarking, but in my tests, it was slightly faster than http.zig.

---

<div class="post-metadata">

**Author:** ![vic-Rokx](https://ziggit.dev/user_avatar/ziggit.dev/vic-rokx/32/4167_2.png) [@vic-Rokx](https://ziggit.dev/u/vic-Rokx)\
**Post date:** [November 3, 2025, 7:54pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/4 "2025-11-03T19:54:35Z")

</div>

When i build in either release safe or fast, i get this error each time I make one request

```zig
➜ dusty git:(main) ✗ zig build --release=safe run
info(dusty): Listening on 127.0.0.1:8080
run
└─ run exe basic-example failure
error: the following command terminated unexpectedly:
./.zig-cache/o/b1572f98f6241b9f71778f855c097645/basic-example

Build Summary: 1/3 steps succeeded; 1 failed
run transitive failure
└─ run exe basic-example failure

error: the following build command failed with exit code 1:
.zig-cache/o/57c6eee1fbbe8e0c6c5790dd27972638/build /Users/vic-augustrokx-nellemann/.zvm/0.15.1/zig /Users/vic-augustrokx-nellemann/.zvm/0.15.1/lib /Users/vic-augustrokx-nellemann/Desktop/Zig/dusty .zig-cache /Users/vic-augustrokx-nellemann/.cache/zig --seed 0x9f1320fe -Z0a44256127c94359 --release=safe run

```

the request

```zig
curl -v http://127.0.0.1:8080/ping

* Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /ping HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 8
<
SUCCESS
* Connection #0 to host 127.0.0.1 left intact

```

But your right best to not benchmark beforehand, was just curious!

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [November 3, 2025, 8:22pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/5 "2025-11-03T20:22:19Z")

</div>

> [@vic-Rokx](#):
>
> When i build in either release safe or fast, i get this error each time I make one request

I’ll need to investigate this, but I have a hunch it might be the same bug I’m just trying to solve. Can I ping you tomorrow to try a new version?

---

<div class="post-metadata">

**Author:** ![vic-Rokx](https://ziggit.dev/user_avatar/ziggit.dev/vic-rokx/32/4167_2.png) [@vic-Rokx](https://ziggit.dev/u/vic-Rokx)\
**Post date:** [November 3, 2025, 8:27pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/6 "2025-11-03T20:27:20Z")

</div>

Yes that’s perfect

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [November 5, 2025, 6:02am UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/7 "2025-11-05T06:02:06Z")

</div>

So yes, the crash you were seeing was almost certainly the `zio.Timeout` problem I was fixing yesterday. It was just manifesting differently on macOS. Now I have an integration test on all platforms that runs the server and checks the output.

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [November 12, 2025, 9:31pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/8 "2025-11-12T21:31:36Z")

</div>

It turns out my previous fix for this didn’t actually fix anything. I now spent some time debugging it after receiving another macOS bug report ([GitHub · Where software is built](https://github.com/lalinsky/zio/issues/135)) and it turns out that this was a LLVM bug / bad behavior. It’s really fixed now.

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [January 9, 2026, 6:56am UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/9 "2026-01-09T06:56:10Z")

</div>

The server now gained support for SSE and WebSockets. These kind of long-running request handlers are where having coroutines underneath is a huge advantage.

And we also have a simple HTTP client now:

```zig
    var client = http.Client.init(allocator, .{});
    defer client.deinit();

    var response = try client.fetch(rt, "http://httpbin.org/get", .{});
    defer response.deinit();

    std.debug.print("Status: {t}\n", .{response.status()});

    std.debug.print("Headers:\n", .{});
    var it = response.headers().iterator();
    while (it.next()) |entry| {
        std.debug.print(" {s}: {s}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
    }

    if (try response.body()) |body| {
        std.debug.print("Body: {s}\n", .{body});
    }

```

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [February 23, 2026, 6:05pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/10 "2026-02-23T18:05:02Z")

</div>

A couple of changes have been done to the project over the last weeks, I think it’s close to its first release:

1. WebSocket client
2. Unix socket support in the client (e.g. for Docker)
3. More complex routing in the server
4. Server middlewares
5. Simple sessions based on a HMAC-signed cookie

I’m not sure whether to keep the server plain, or turn it into more into a web framework, but I think it will eventually end up being a small web framework on a level similar to e.g. [Starlette](https://starlette.dev).

I want to add JSON/MsgPack support, and possibly even integrate it with [zt](https://github.com/lalinsky/zt) (once I’m done experimenting with the project).

---

<div class="post-metadata">

**Author:** ![zigster](https://ziggit.dev/user_avatar/ziggit.dev/zigster/32/51_2.png) [@zigster](https://ziggit.dev/u/zigster)\
**Post date:** [February 26, 2026, 8:55pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/11 "2026-02-26T20:55:53Z")

</div>

Nice work, once again

Really interesting question about keeping it plain vs growing it into an opinionated framework. I guess you need to consider the audience.. and at the end of the day, the most important audience is yourself.

Let’s assume that your motivation here is that you have a massive backlog of ideas for apps that have been bugging you for years, waiting for the perfect tools to build them with. You are almost done now, after 6 months off, building everything to suit - from zio, nats client, redis, pg, zt, all the way up to Dusty.

May as well keep going, and push all the common problems for your own apps, such as auth, etc into your framework - because it’s your framework after all. Be as opinionated as you need it to be for your own goals.

If opinionated patterns are useful for you, they are probably going to be just as useful for others as well.

You won’t be locking anyone into a box, because it’s all open source - they are free to extract a subset of what you are building to suit possibly different needs if that’s where they are at.

That’s just my 2c opinion - go for it, and grow it into the perfect opinionated framework for exactly what you need to get on with your own apps.

---

<div class="post-metadata">

**Author:** ![awesomo4000](https://ziggit.dev/user_avatar/ziggit.dev/awesomo4000/32/543_2.png) [@awesomo4000](https://ziggit.dev/u/awesomo4000)\
**Post date:** [March 1, 2026, 8:29pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/12 "2026-03-01T20:29:51Z")

</div>

You could leave the server separate and have the framework depend on it. Or depend on an interface that the server provides. It’s nice to be the caller and not the callee when using other code. i.e. use a library and not be committed to a framework.

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [March 2, 2026, 6:17am UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/13 "2026-03-02T06:17:38Z")

</div>

It’s never going to be a full blown web framework, with templates, database connections, etc. I don’t see the value in that.

However, it will likely gain more high-level features like session management API (extendable), authentication API (also extendable).

I think Starlette in Python land is a good example of minimal HTTP server library, that still has batteries included.

---

<div class="post-metadata">

**Author:** ![nurulhudaapon](https://ziggit.dev/user_avatar/ziggit.dev/nurulhudaapon/32/5148_2.png) [@nurulhudaapon](https://ziggit.dev/u/nurulhudaapon)\
**Post date:** [March 2, 2026, 7:31pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/14 "2026-03-02T19:31:58Z")

</div>

Have been looking forward to dusty! Would love to get dusty added in the benchmark in here: [https://zigweb.nuhu.dev](https://zigweb.nuhu.dev)

Also what is your olan with migrating dusty to [std.Io](http://std.Io)?

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [March 2, 2026, 7:52pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/15 "2026-03-02T19:52:07Z")

</div>

> [@nurulhudaapon](#):
>
> Have been looking forward to dusty! Would love to get dusty added in the benchmark in here: [https://zigweb.nuhu.dev](https://zigweb.nuhu.dev)

I’ve not benchmarked it too much, so I don’t know how exactly it will do. For a simple hello world request, I don’t expect it to be the fastest. The benefit of Dusty is scalability when you actually wait on database / HTTP client / WebSocket.

> [@nurulhudaapon](#):
>
> Also what is your plan with migrating dusty to [std.Io](http://std.Io)?

I’ll do it once 0.16 is released. I expect `std.Io` will get shipped without proper timeout support, so there will be still some zio-specific bits in there, as I don’t want to cripple the server for my use cases, but it will be build-time optional.

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [April 28, 2026, 9:36am UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/16 "2026-04-28T09:36:20Z")

</div>

I’ve released v0.2.0 of Dusty, the main change is upgrade to Zig 0.16.

The code still depends on zio, because `std.Io` lacks proper timeouts, but the server exposes `io` instance for you to use in request handlers, so you can have request handlers like this:

```zig
fn handleUser(req: *http.Request, res: *http.Response) !void {
    const user_id = req.params.get("id") orelse "guest";
    try req.io.sleep(.fromMilliseconds(10), .real);
    try res.json(.{ .id = user_id, .name = "John Doe" }, .{});
}

```

The `Io` instance uses zio’s implementation of the interface and any library you use it with will use asynchronous I/O, so you can do even long blocking operations.

> **[GitHub - lalinsky/dusty: HTTP client/server library for Zig](https://github.com/lalinsky/dusty#server-example)**
>
> HTTP client/server library for Zig

---

<div class="post-metadata">

**Author:** ![Illusion](https://ziggit.dev/letter_avatar_proxy/v4/letter/i/e19b73/32.png) [@Illusion](https://ziggit.dev/u/Illusion)\
**Post date:** [April 29, 2026, 7:14pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/17 "2026-04-29T19:14:47Z")

</div>

Just so that I’m understanding right, this exposed `io` could then be utilized with the Dusty HTTP client to provide asynchronous calls? E.g.

```zig
fn handleUser(req: *http.Request, res: *http.Response) !void {
    const user_id = req.params.get("id") orelse "guest";
    var db_future = req.io.async(queryDB, .{io, dusty_client1, user_id});
    var registry_future = req.io.async(queryRegistry, .{io, dusty_client2, user_id});
    const user_db = db_future.await(req.io);
    const registry_db = registry_future.await(req.io);
    ...
    try res.json(.{ .id = user_id, .name = "John Doe", /* more fields */ }, .{});
}

```

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [April 29, 2026, 8:45pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/18 "2026-04-29T20:45:13Z")

</div>

Yes, you can use it like that. That was the main advantage of dusty/zio even before the Io interface got introduced. The `io.async` in your code is equivalent to `zio.spawn`, both are fine.

---

<div class="post-metadata">

**Author:** ![Illusion](https://ziggit.dev/letter_avatar_proxy/v4/letter/i/e19b73/32.png) [@Illusion](https://ziggit.dev/u/Illusion)\
**Post date:** [April 29, 2026, 8:56pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/19 "2026-04-29T20:56:51Z")

</div>

Cool, thanks. One thing I’d curious about is if you’d view pulling out the underlying tls client as an improvement. I.e., presently, on each new `Connection` if it’s a TLS caring connection, you utilize [dusty/src/client.zig at cbed18d85f51bf7b7b2e6e13ebddf73f7522fd51 · lalinsky/dusty · GitHub](https://github.com/lalinsky/dusty/blob/cbed18d85f51bf7b7b2e6e13ebddf73f7522fd51/src/client.zig#L334)

i.e. `std.crypto.tls.Client.init`

If it was pulled out in some way, perhaps ianic’s `tls` module could be used in its place (useful for people like me for which the TLS implementation of the stdlib doesn’t work). Just musing at the moment though, would still have to experiment and see if method signature matches between the two.

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [April 29, 2026, 9:00pm UTC](https://ziggit.dev/t/dusty-http-server-client-library/12807/20 "2026-04-29T21:00:23Z")

</div>

I definitely would. I’m not particularly happy with the stdlib TLS client. To be honest, I could even just replace it with the external lib. I’ll have a look how it could be extracted. Should be possible if they both provide Io.Reader/Writer interfaces.

[Next page](https://ziggit.dev/t/dusty-http-server-client-library/12807.md?page=2)
