Changes to http.zig

TL;DR

Use the blocking branch if:

  • You need Windows support
  • You're an existing happy user of the master branch

Use the master branch if:

  • You need better performance and scaling
  • Expect thousands+ of concurrent requests
  • Are on linux/bsd/macos

In either case, http.zig should be deployed behind a robust reverse proxy (e.g. NGINX).

Background

http.zig was created directly as a result of async being removed from Zig. At the time most (all?) existing implementations broke and updates didn't seem forthcoming. I wrote http.zig to fill the gap.

Blocking

http.zig initially used a thread-per-connection with blocking sockets. It's simple to understand and develop and works on all platforms. Under moderate load, this version of http.zig performed well. However, under high concurrency, thread thrashing becomes a problem, memory usage is high, and latency increases. The thread_pool option was added (as a PR, thank you) to make the performance more predictable.

This version of http.zig, previously on the master branch, is now on the blocking branch. If you need Windows support, are an existing happy user of http.zig, aren't overly concerned about load or malicious clients (which can largely be mitigated by using a properly configured reversed proxy), then consider using the blocking branch. It's simpler and, relatively speaking, more battle tested.

My own concern with blocking sockets has always been exposure to real-world networking: slow and malicious clients.

Nonblocking

The master branch now uses nonblocking sockets (epoll on linux and kqueue on BSD and MacOS). This creates a more robust server - a slow client won't impact other clients. The new timeout configurations, while basic, can help protect http.zig from simple DOS attacks.

Your application handlers continue to be executed synchronously. This isn't efficient, but I don't see a realistic solution. We have to wait for async to be re-added to the language. At least now the performance/latency is largely a matter of the application code, as opposed to those untrustworthy clients!

Future enhancements are more likely to happen on the master branch.

Comments

z1fire

I'm still new to Zig and learning, and I want to say thank you for the great learning material you've put out. I've been looking at your repo and looking at Zap. I want to build a similar blog to this one, kind of like medium but free but specifically geared towards posting tutorials for Zig. I already have the domain zigtutorials.org. Would you happen to have any advice for me?

karlseguin

I don't have any specific advice. I think at this point, things are relatively straightforward in the Zig ecosystem. If you plan on using PostgreSQL, I wrote a native postgresql library you might like: https://www.github.com/karlseguin/pg.zig

For markdown, I used cmark-gmf: https://github.com/github/cmark-gfm. This was the extend of my wrapper:

const std = @import("std");
const c = @cImport(@cInclude("cmark-aolium.h"));

pub fn init() void {
	c.init();
}

pub fn deinit() void {
	c.deinit();
}

pub fn toHTML(input: [*:0]const u8, len: usize) Result {
	return .{
		.value = c.markdown_to_html(input, len),
	};
}

pub const Result = struct {
	value: [*:0]u8,

	pub fn deinit(self: Result) void {
		std.c.free(@ptrCast(self.value));
	}
};

z1fire

Thanks for the fast response, I've definitely been looking through your repos.

u3pbq5711

Hello, interesting Zig libraries!, did you compare http.zig benchmark with some other? like for example Zig Zap, or another Rust Axum..

karlseguin

As far as I know, http.zig is currently the fastest pure Zig HTTP server implementation. Without a concurrency story baked into the language though, it cannot compete with high-performance frameworks in other languages. The issue is that the application handler is executed synchronously on a http.zig worker thread. This prevents the worker thread from doing other things (like accepting and parsing new/other requests). If the application handler is waiting (e.g. on a database result), then the http.zig worker blocks even though there's plenty of other work to do.

In my tests, Zap is about 15% faster. It (or more specifically, facil.io) are also battle tested and offer more features (e.g. TLS). But I'm pretty happy being within that range of facil.io.

kassane

Do you have an example that uses pg.zig with http.zig?

karlseguin

I don't, but if you look at the existing global example (examples/global.zig). I would:

  1. Change the GlocalContext to have a db_pool: *pg.Pool
const GlobalContext = struct {
    db_pool: *pg.Pool,
    hits: usize = 0,
    l: std.Thread.Mutex = .{},
    // ...
  1. Initialize the pool and use that when creating the context:
const db_pool = try pg.Pool.init(allocator, .{
  .size = 5,
  .connect = .{.port = 5432, .host = "127.0.0.1"},
  .auth = .{.username = "X", .database = "Y", .password = "Z"}
}) 
defer db_pool.deinit();

var ctx = GlobalContext{
    .db_pool = db_pool,
};

3. And then in your handlers, you can access the pool:

pub fn increment(ctx: *GlobalContext, _: *httpz.Request, res: *httpz.Response) !void { var rows = try ctx.db_pool.query("", .{...}); defer rows.deinit()

// or, explicit: const conn = try pool.acquire(); defer pool.release(conn); conn.exec("...", .{...}); }

Leave a Comment

All comments are reviewed before being made public.