Skip to content

Release Modes

Zig can build the same source code in different optimization modes.

Zig can build the same source code in different optimization modes.

The mode controls speed, size, safety checks, and debug information.

The four common modes are:

ModeMain purpose
DebugFast compilation, safety checks, useful debug info
ReleaseSafeOptimized code with safety checks
ReleaseFastOptimized code for speed
ReleaseSmallOptimized code for small size

With zig build-exe, the default is Debug.

zig build-exe main.zig

To select a release mode, use -O:

zig build-exe main.zig -O Debug
zig build-exe main.zig -O ReleaseSafe
zig build-exe main.zig -O ReleaseFast
zig build-exe main.zig -O ReleaseSmall

In Debug, Zig keeps runtime safety checks.

For example, this program indexes past the end of an array:

const std = @import("std");

pub fn main() void {
    const a = [_]u8{ 1, 2, 3 };
    const i: usize = 9;

    std.debug.print("{d}\n", .{a[i]});
}

In a checked mode, this is caught as a safety problem.

Safety checks include bounds checks, integer overflow checks, invalid enum values, invalid error values, and other operations that Zig can guard at runtime.

ReleaseSafe keeps safety checks but optimizes the program.

zig build-exe main.zig -O ReleaseSafe

This is useful for production programs where correctness diagnostics are more important than the last measure of speed.

ReleaseFast optimizes for speed and disables many runtime safety checks.

zig build-exe main.zig -O ReleaseFast

This mode is appropriate only when the program has been tested enough that unchecked operations are acceptable.

ReleaseSmall optimizes for size.

zig build-exe main.zig -O ReleaseSmall

This is useful for embedded programs, WebAssembly, small command-line tools, and places where binary size matters.

The selected mode is visible at compile time:

const std = @import("std");
const builtin = @import("builtin");

pub fn main() void {
    std.debug.print("mode = {s}\n", .{@tagName(builtin.mode)});
}

This can be used to choose different behavior.

const builtin = @import("builtin");

pub fn expensiveCheck() bool {
    if (builtin.mode == .Debug or builtin.mode == .ReleaseSafe) {
        return true;
    } else {
        return false;
    }
}

Use this sparingly. Most code should not change behavior based on optimization mode.

A better use is diagnostics:

const std = @import("std");
const builtin = @import("builtin");

pub fn trace(comptime fmt: []const u8, args: anytype) void {
    if (builtin.mode == .Debug) {
        std.debug.print(fmt, args);
    }
}

In Debug, trace prints. In release builds, it does nothing.

Build mode is also part of the standard build.zig pattern:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "main",
        .root_module = b.createModule(.{
            .root_source_file = b.path("main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(exe);
}

Then the mode is chosen from the command line:

zig build -Doptimize=Debug
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast
zig build -Doptimize=ReleaseSmall

The build file describes the program. The command line selects how it is built.

A good rule is:

Debug while writing.
ReleaseSafe while testing serious builds.
ReleaseFast when speed has been measured.
ReleaseSmall when size has been measured.

Do not choose a release mode by habit. Choose it for the property you need.

Exercise 17-21. Print builtin.mode in each optimization mode.

Exercise 17-22. Write an array bounds error and compare Debug with ReleaseFast.

Exercise 17-23. Write a trace function that prints only in Debug.

Exercise 17-24. Build the same program with ReleaseFast and ReleaseSmall, then compare file sizes.