Zig on Workers: From Laptop to Edge in Three Steps

With WASI support now available in Workers, Cloudflare's serverless platform is no longer limited to JavaScript and WebAssembly compiled from C-like languages. I wanted to test just how far that polyglot promise goes by taking Zig—a language I'm only beginning to learn—from a blank terminal to a deployed Worker.

Building a Minimal Zig CLI

My test program was deliberately simple: a clone of cat -n that reads lines from stdin and writes them to stdout with line numbers. The point isn't the utility—it's verifying that the same binary runs identically on a laptop and as an HTTP endpoint on the edge.

const std = @import("std");

pub fn main() anyerror!void {
	// setup allocator
	var gpa = std.heap.GeneralPurposeAllocator(.{}){};
	defer std.debug.assert(!gpa.deinit());
	const allocator = gpa.allocator();

	// setup streams
	const stdout = std.io.getStdOut().writer();
	const in = std.io.getStdIn();
	var reader = std.io.bufferedReader(in.reader()).reader();

	var counter: u32 = 1;

	// read input line by line
	while (try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', std.math.maxInt(usize))) |line| {
    	    defer allocator.free(line);
    	    try stdout.print("{d}\t{s}\n", .{counter, line});
    	    counter = counter + 1;
	}
}

Zig projects are defined through a build.zig file. For a single executable, this file only needs to specify the source. Running zig build then produces the native binary in zig-out/bin.

const std = @import("std");

pub fn build(b: *std.build.Builder) void {
	const target = b.standardTargetOptions(.{});
	const mode = b.standardReleaseOptions();

	const exe = b.addExecutable("print-with-line-numbers", "src/main.zig");
	exe.setTarget(target);
	exe.setBuildMode(mode);
	exe.install();
}
$ zig build

$ ls zig-out/bin
print-with-line-numbers

$ echo "Hello\nWorld" | ./zig-out/bin/print-with-line-numbers
1    Hello
2    World

Compiling for WebAssembly

Recent Zig versions ship with built-in WebAssembly support, so no extra tooling is required. You simply pass the wasm32-wasi target to the compiler, and Zig outputs a .wasm file compatible with any WASI runtime, including wasmtime. That same file can be tested locally and then uploaded directly to Workers—no code changes, no separate compilation step for production.

$ zig build -Dtarget=wasm32-wasi

$ ls zig-out/bin
print-with-line-numbers.wasm

$ echo "Hello\nWorld" | wasmtime ./zig-out/bin/print-with-line-numbers.wasm
1    Hello
2    World

Deploying with Wrangler 2

Deployment takes advantage of wrangler2. After signing up for a workers.dev account and following the getting started guide, there are only two prerequisites: installing wrangler and logging in. From there, a single command publishes the .wasm file to the edge.

$ npx wrangler@wasm login
Attempting to login via OAuth...
Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth
Successfully logged in.
$ npx wrangler@wasm publish --name print-with-line-numbers --compatibility-date=2022-07-07 zig-out/bin/print-with-line-numbers.wasm
Uploaded print-with-line-numbers (3.04 sec)
Published print-with-line-numbers (6.28 sec)
  print-with-line-numbers.workers.dev

The output includes the URL for the newly deployed worker; hitting that URL runs the Zig binary and returns its output.

echo "Hello\nWorld" | curl https://print-with-line-numbers.workers.dev -X POST --data-binary @-
1    Hello
2    World

What This Demonstrates

The most surprising part of this exercise was how frictionless the entire path was. Native compilation came first, then WebAssembly with a single compiler flag, and finally edge deployment without touching a line of code. The program itself is trivial—just standard I/O through WASI—but the workflow shows how the WebAssembly ecosystem, together with Workers, is lowering the barrier for any language with a WASI target. As the technology matures, the scope of what can run unchanged on the edge will only grow.