Skip to content
Dev Tools Article

Zig's 50ms Rebuilds Rest on Patching Binaries in Place

A core-team internals writeup shows the hard part was never caching — it was incremental linking.

Lenn Voss
Lenn Voss
Cloud & Infrastructure Writer · Jul 28, 2026 · 5 min read
Zig's 50ms Rebuilds Rest on Patching Binaries in Place

Matthew Lugg, the Zig core team member who built much of the compiler's incremental compilation support, has published a rare thing: a detailed internals writeup of a system most toolchains have quietly given up on. The headline numbers are startling. A real application — a pixel editor — takes about 5 seconds for its initial debug build, then rebuilds in 50–70ms on every subsequent change. In one traced update, semantic analysis took 1.2ms, code generation 240µs, and linking 170µs. The binary on disk gets patched in place.

That last part is the story. Everything else in the post — per-file IR caching, dependency graphs, hashed source ranges — has prior art in every serious compiler. Patching machine code directly into the output executable, function by function, is the part nobody else ships.

Caching is table stakes; linking is the wall

It's worth being precise about what's novel here, because "incremental compilation" is a phrase every compiler team claims. GCC users have had ccache for decades. Rust's compiler has had incremental compilation since 2016, built on a query system that memoizes analysis results and only recomputes what a change invalidates. Zig's own first stage does the standard thing too: each source file is parsed and lowered to an untyped SSA IR called ZIR, cached per file, so unchanged files cost nothing. Lugg notes that regenerating ZIR for the entire Zig compiler codebase takes about 920ms without any parallelism — fast, but as he admits, lots of compilers can do this kind of caching.

The wall everyone hits afterward is the back half of the pipeline. Rust's incremental machinery caches queries and object code, but then hands everything to a conventional linker, which relinks the whole binary. On a large project, that link step dominates the edit-compile cycle no matter how clever the front end is — it's why the Rust ecosystem has burned so much energy on mold, lld-by-default, and lately hot-patching hacks like Dioxus's subsecond. Lugg's diagnosis is blunt: incremental linking is hard, and he suspects it's the main reason no other major toolchain does compilation at this granularity.

Zig's answer is a linker abstraction called MappedFile that memory-maps the output binary and manages it as a tree of resizable regions. When a function's machine code changes, its region is rewritten; if it grows past its allocation, it relocates, with exponential padding to amortize future growth. The unit of recompilation isn't the file or the crate — it's the individual function or declaration, tracked through a dependency graph over four kinds of "analysis units" (type layouts, declaration types, comptime values, function bodies). Change one function and, in the best case, one function gets re-analyzed, re-generated, and stitched into the existing executable.

The prior art that failed

If this sounds familiar to Windows veterans, it should. MSVC has shipped incremental linking and Edit and Continue since the '90s, using function padding and jump thunks — conceptually the same trick. It earned a reputation for corrupt .ilk files, mysterious thunk overhead, and "do a full rebuild and see if the bug goes away" folklore, and it's routinely disabled in CI. Before that, Turbo Pascal, Smalltalk, and Lisp images made sub-second redefinition normal — by owning the entire toolchain and runtime.

That's the actual lesson in Lugg's post, even though he doesn't frame it this way: incremental compilation at function granularity is only tractable when one team controls the parser, the semantic analyzer, the code generator, and the linker, and designs them together. Zig spent years getting there. The self-hosted x86_64 backend — no LLVM in the loop — became the default for debug builds on Linux and macOS in 0.15.1, and by 0.16.0 (April 2026) it built code roughly 5× faster than the LLVM path. A new self-hosted ELF linker landed alongside it. The language itself has been tweaked over the years specifically to make dependency tracking feasible; the post walks through why comptime and inline calls make this genuinely hard (an inline call makes the caller depend on the callee's source, not just its signature).

Rust can't easily copy this. rustc's query system is arguably more sophisticated than Zig's dependency graph, but it sits in front of LLVM and an external linker it doesn't control, and the language's monomorphization model smears one change across many codegen units. The honest comparison isn't "Zig is faster than Rust" — it's that Zig bought itself the option of true incrementality by paying the enormous cost of self-hosted backends first.

Should you actually use it?

Here's the practical state, and the caveats matter. The flag is:

zig build --watch -fincremental

You want a master-branch build (or 0.17.0 once it ships) — 0.16.0 has the feature but is missing linker pieces. Full support means x86_64 Linux with the self-hosted ELF path; other targets' backends aren't mature enough yet. And Lugg is candid that the system is not stable: expect occasional false-positive compile errors and, more seriously, possible miscompilations.

That caveat frames the right adoption posture. Incremental mode today is a development-loop tool — an extremely fast type-checker and smoke-tester, with --watch giving you near-instant compile errors as you type. Most of the Zig core team reportedly uses it daily in exactly that mode. What it is not is something to trust for release artifacts or bit-reproducible builds; do a clean build before you ship, same as you always should have with MSVC's incremental linker.

My read: this is a genuine shift, not a demo. The numbers are from real projects, the design is documented in enough detail to be falsifiable, and the remaining gaps (platform coverage, stability) are the boring kind that release cycles grind down. The strategic effect may land outside Zig entirely — 50ms rebuilds reset developers' expectations the way Vite's hot reload did for the web, and every AOT toolchain that shells out to a black-box linker will eventually be asked why an edit-build-run cycle takes 15 seconds when Zig does it before your finger leaves the key. The uncomfortable answer, per Lugg, is that they'd have to own their whole pipeline to fix it. Zig already does.

Sources & further reading

  1. Zig's Incremental Compilation Internals — mlugg.co.uk
  2. Zig 0.16.0 Release Notes — ziglang.org
  3. Incremental Compilation - Rust Compiler Development Guide — rustc-dev-guide.rust-lang.org
Lenn Voss
Written by
Lenn Voss · Cloud & Infrastructure Writer

Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading