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.



