Zig comptime Is the Future of Systems Programming

Generics solved at compile time with zero runtime cost. Every language that ships a runtime should be embarrassed.

By Anokuro Engineering··Infrastructure

Every language that ships a garbage collector, a runtime, or a JIT compiler is carrying dead weight. We have known this for decades. What we did not have, until Zig, was a language that eliminated that weight without making the programmer suffer for it.

Zig's comptime is not a feature. It is a paradigm shift. It takes the entire computation model of the language and makes it available at compile time. Not a subset. Not a restricted DSL. The same language, the same semantics, running during compilation with zero cost at runtime.

We use Zig to build AnokuroDB, our in-house storage engine for ad-serving. The codebase is roughly 48,000 lines of Zig. Comptime is in every layer.

What comptime Actually Is

C++ templates are a Turing-complete accident. They were not designed to be a compile-time programming language; they evolved into one through a series of increasingly baroque extensions. Writing a compile-time hash map in C++ requires template metaprogramming that looks like it was generated by an adversarial AI.

Rust's const generics are a deliberate improvement, but they are still limited. You can parameterize over values, but const functions in Rust cannot do everything regular functions can. There is a boundary between "const context" and "runtime context" that you hit constantly.

Zig has no boundary. A function marked comptime runs the same Zig code during compilation. You can allocate memory, iterate over slices, build data structures, and return types. The result is baked into the binary. At runtime, it does not exist as computation. It exists as data.

Here is the mental model: comptime is an interpreter for Zig that runs inside the compiler. The output of that interpreter is specialized code and static data. The runtime binary contains only the output, never the computation that produced it.

Real Examples From Our Codebase

Compile-Time Serialization

Every record in AnokuroDB goes through a serialization layer. The naive approach is to write serialize/deserialize functions for each record type by hand. The correct approach is to derive them at comptime.

fn Serializer(comptime T: type) type {
    const fields = @typeInfo(T).@"struct".fields;
    return struct {
        pub fn serialize(value: T, buf: []u8) usize {
            var offset: usize = 0;
            inline for (fields) |field| {
                const val = @field(value, field.name);
                offset += writeField(field.type, val, buf[offset..]);
            }
            return offset;
        }

        pub fn deserialize(buf: []const u8) T {
            var result: T = undefined;
            var offset: usize = 0;
            inline for (fields) |field| {
                @field(result, field.name) = readField(field.type, buf[offset..], &offset);
            }
            return result;
        }
    };
}

This generates specialized serialization code for every struct type we use. No reflection at runtime. No virtual dispatch. No format strings. The compiled output is identical to hand-written, type-specific serialization code. We verified this by comparing the assembly.

Compile-Time Query Parsing

Our internal query protocol uses a binary format with 6 operation types. Each operation has a different payload layout. We define the protocol once as comptime data, and the parser is generated from that definition:

const ops = .{
    .{ .code = 0x01, .name = "point_get",   .payload = PointGetPayload },
    .{ .code = 0x02, .name = "multi_get",   .payload = MultiGetPayload },
    .{ .code = 0x03, .name = "range_scan",  .payload = RangeScanPayload },
    .{ .code = 0x04, .name = "put",         .payload = PutPayload },
    .{ .code = 0x05, .name = "delete",      .payload = DeletePayload },
    .{ .code = 0x06, .name = "batch_write", .payload = BatchWritePayload },
};

fn parseOp(buf: []const u8) !Operation {
    const code = buf[0];
    inline for (ops) |op| {
        if (code == op.code) {
            const payload = Serializer(op.payload).deserialize(buf[1..]);
            return @unionInit(Operation, op.name, payload);
        }
    }
    return error.UnknownOpCode;
}

The inline for unrolls at compile time. The generated code is a flat sequence of comparisons with specialized deserialization for each branch. No jump tables, no function pointer arrays, no dynamic dispatch. The branch predictor loves this.

Compile-Time Hash Maps

We use compile-time perfect hash maps for configuration lookups and error code translation. Zig's std.StaticStringMap builds a perfect hash function at comptime:

const error_messages = std.StaticStringMap([]const u8).initComptime(.{
    .{ "E001", "segment not found" },
    .{ "E002", "bid timeout exceeded" },
    .{ "E003", "creative format mismatch" },
    // ... 47 more entries
});

The hash function is computed once during compilation. At runtime, lookups are O(1) with no hash collisions. The entire map is a static array in the binary's read-only data segment.

The Benchmarks

We measured the performance impact of comptime dispatch versus virtual dispatch in our storage engine's hot path. The hot path processes every bid request. It runs 200,000+ times per second.

Virtual dispatch (function pointer in a vtable, as you would do in C++ or Rust trait objects):

  • Mean: 3.2ns per call
  • P99: 8.1ns (cache miss on the vtable)

Comptime dispatch (inline for with comptime type specialization):

  • Mean: 0.4ns per call
  • P99: 0.6ns

That is an 8x improvement on the mean and a 13.5x improvement on P99. On a single call. In a loop that runs 200k times per second, the comptime path saves 560 microseconds per second. Across an entire request pipeline with dozens of dispatch points, comptime dispatch saved us 15-40% of CPU time in tight loops depending on the workload.

We measured this with perf stat counting cycles, not wall-clock time. The numbers are real.

The Zig Memory Model

Zig does not have a default allocator. Every function that allocates memory takes an Allocator parameter. This is not a suggestion. It is the language's design.

In AnokuroDB, we use four allocator types:

  • Arena allocators for request-scoped memory. A bid request arrives, we allocate an arena, process the request, and free the entire arena in one operation. No per-object deallocation. No fragmentation.
  • Pool allocators for fixed-size objects like B-tree nodes and SSTable index entries. Allocation is O(1). Deallocation is O(1). Memory reuse is automatic.
  • Page allocators for large, aligned allocations that back the block cache and memtable. These go directly to mmap.
  • The general-purpose allocator for startup configuration and one-time initialization. It never appears in the hot path.

This explicitness has a cost: every function signature is slightly more verbose. It also has a benefit: we have never had a memory leak in production. Not because we are brilliant, but because the allocator model makes leaks structurally impossible when you use arenas for request scoping. If the arena frees, everything in it frees.

The Hot Take

Garbage-collected languages have no place in infrastructure that serves latency-sensitive traffic. GC pauses are non-deterministic. You cannot bound them. You can tune them, and tuning a GC is the engineering equivalent of negotiating with a hostage-taker: you might get a good outcome, but you are not in control.

Go's GC pauses are short. Java's ZGC pauses are shorter. They are still pauses. They still show up in P99 latency. When your latency budget is 2ms and a GC pause costs 0.5ms, you have lost 25% of your budget to a decision the language runtime made without asking you.

We do not use Go for anything that serves traffic. We do not use Java. We do not use C#. These are fine languages for many things. Serving 200,000 ad auctions per second with single-digit millisecond latency is not one of those things.

Zig gives us the control of C with the expressiveness of a language designed in this century. Comptime gives us zero-cost abstractions that are actually zero-cost, not "zero-cost with a footnote about monomorphization bloat." The compiler does exactly what we tell it. Nothing more. Nothing less.

Every language that ships a runtime should look at what Zig's comptime achieves and ask themselves: why are we making our users pay for something that could have been resolved before the binary was built?

Copyright © 2026 Anokuro Pvt. Ltd. Singapore. All rights reserved.