# How Lightpanda's CDP Command Dispatch System Works: Zero-Cost Routing in Zig

> Discover how Lightpanda's CDP command dispatch system uses zero-cost routing in Zig to efficiently process JSON-RPC messages through compile-time bit-casting and domain-specific handlers.

- Repository: [Lightpanda/browser](https://github.com/lightpanda-io/browser)
- Tags: internals
- Published: 2026-03-14

---

**Lightpanda's CDP command dispatch system parses JSON-RPC messages through a four-stage pipeline that validates sessions against a special STARTUP pseudo-session or active BrowserContext, then routes commands to domain-specific handlers using compile-time integer bit-casting to eliminate runtime string hashing.**

The lightpanda-io/browser project implements a high-performance Chrome DevTools Protocol (CDP) server in Zig that handles automation commands from headless drivers and test harnesses. Understanding how Lightpanda's CDP command dispatch system processes incoming messages reveals a sophisticated architecture optimized for speed and type safety through compile-time code generation.

## Message Processing Pipeline

The dispatch logic in `src/cdp/cdp.zig` processes every CDP message through four distinct stages, from raw WebSocket payload to domain-specific execution.

### JSON Parsing and InputMessage Deserialization

The entry point `processMessage` receives raw JSON strings and deserializes them into a structured `InputMessage` type. This struct captures the standard CDP request shape including `id`, `method`, optional `params`, and optional `sessionId`.

```zig
pub fn processMessage(self: *Self, msg: []const u8) !void {
    const arena = &self.message_arena;
    defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 });
    return self.dispatch(arena.allocator(), self, msg);
}

```

The implementation uses `json.parseFromSliceLeaky` to populate `InputMessage` (lines 41-45 in `src/cdp/cdp.zig`), leveraging an arena allocator that retains up to 16KB of memory between requests to minimize allocation overhead.

### Session Validation and the STARTUP Pseudo-Session

Before routing to domain handlers, the system validates the `sessionId` against active browser contexts. Lightpanda recognizes a special `"STARTUP"` pseudo-session used before any `BrowserContext` exists, alongside standard session UUIDs.

```zig
if (input.sessionId) |input_session_id| {
    if (std.mem.eql(u8, input_session_id, "STARTUP")) {
        is_startup = true;
    } else if (self.isValidSessionId(input_session_id) == false) {
        return command.sendError(-32001, "Unknown sessionId", .{});
    }
}

```

The `isValidSessionId` method verifies normal sessions against the current `BrowserContext.session_id`, returning a CDP error code `-32001` for invalid sessions (lines 60-68 in `src/cdp/cdp.zig`).

### Zero-Cost Domain Routing with Compile-Time Bit-Casting

The routing mechanism eliminates runtime string comparisons by bit-casting domain names to integers at compile time. The method string splits at the first `.` character, separating the domain (e.g., `Network`, `Page`) from the action (e.g., `enable`, `getFrameTree`).

```zig
const domain = blk: {
    const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse {
        return error.InvalidMethod;
    };
    command.input.action = method[i + 1 ..];
    break :blk method[0..i];
};

switch (domain.len) {
    2 => switch (@as(u16, @bitCast(domain[0..2].*))) {
        asUint(u16, "LP") => return @import("domains/lp.zig").processMessage(command),
        else => {},
    },
    3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
        asUint(u24, "DOM") => return @import("domains/dom.zig").processMessage(command),
    },
    7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
        asUint(u56, "Browser") => return @import("domains/browser.zig").processMessage(command),
        asUint(u56, "Runtime")  => return @import("domains/runtime.zig").processMessage(command),
    },
}

```

This technique uses `@bitCast` to convert domain string slices into fixed-width integers (`u16`, `u24`, `u56`), enabling a two-level switch statement that resolves routing at compile time with zero runtime hashing cost (lines 120-160 in `src/cdp/cdp.zig`).

### Response Serialization and Event Emission

The generic `Command` struct encapsulates the request context and provides type-safe helpers for generating JSON-RPC responses. The `sendResult` method serializes return values while `sendError` handles protocol errors.

```zig
pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void {
    return self.sender.sendJSON(.{
        .id = self.input.id,
        .result = if (comptime @typeInfo(@TypeOf(result)) == .null) struct {}{} else result,
        .sessionId = if (opts.include_session_id) self.input.session_id else null,
    });
}

```

Domain implementations receive the `Command` object and invoke these helpers to return data or emit events like `Network.requestWillBeSent` through `sendEvent`.

## Domain Implementation Architecture

Each CDP domain resides in a dedicated Zig module under `src/cdp/domains/`, implementing a consistent interface that separates protocol plumbing from business logic.

### The Command Generic Interface

The `Command` type passed to domain handlers acts as a generic wrapper around the request context. It carries the parsed `InputMessage`, the session state, and references to the output sink. This design allows domains to remain agnostic of transport details while maintaining type safety through Zig's compile-time generics.

### Domain Module Structure

Every domain module exports a `processMessage` function that receives the generic `Command` and returns `!void`. For example, the `Page` domain in `src/cdp/domains/page.zig` handles navigation and frame tree requests:

```zig
pub fn processMessage(command: anytype) !void {
    switch (command.input.action) {
        "getFrameTree" => {
            return command.sendResult(.{
                .frameTree = .{ .frame = .{
                    .id = "TID-STARTUP",
                    .loaderId = "LOADERID24DD2FD56CF1EF33C965C79C",
                    .securityOrigin = cdp.URL_BASE,
                    .url = "about:blank",
                    .secureContextType = "Secure",
                } },
            }, .{});
        },
    }
}

```

The `Runtime`, `Network`, `Fetch`, and `Target` domains follow identical patterns in their respective files (`runtime.zig`, `network.zig`, `fetch.zig`, `target.zig`), enabling modular extension of the CDP surface area.

## Testing and Practical Usage

The `src/cdp/testing.zig` module provides a test harness with a mock `Client` implementation, allowing unit tests to verify command dispatch without a live WebSocket connection.

### Testing CDP Commands with the Test Harness

The testing framework exposes `processMessage` helpers that serialize structs to JSON and validate responses:

```zig
var ctx = testing.context();
defer ctx.deinit();

const bc = try ctx.loadBrowserContext(.{ .url = "test.html" });

try ctx.processMessage(.{
    .id = 1,
    .method = "Network.enable",
    .sessionId = bc.session_id,
});
try ctx.expectSentResult(null, .{ .id = 1, .index = 0, .session_id = bc.session_id });

```

This pattern validates that the dispatch system correctly routes `Network.enable` to the appropriate domain handler and generates the expected JSON-RPC response structure.

### Emitting CDP Events from Domain Handlers

Domains emit asynchronous events through the `sendEvent` method available on the CDP instance. The `Network` domain uses this to signal request lifecycle changes:

```zig
pub fn httpRequestStart(self: *BrowserContext, msg: *Notification.RequestStart) !void {
    try self.cdp.sendEvent("Network.requestWillBeSent", .{
        .requestId = requestId,
        .url = msg.url,
        .timestamp = std.time.timestamp(),
    }, .{ .session_id = self.session_id });
}

```

Events inherit the same serialization logic as command responses but omit the `id` field per JSON-RPC notification specifications.

## Key Source Files

Understanding the dispatch system requires familiarity with these core files in the lightpanda-io/browser repository:

- **`src/cdp/cdp.zig`**: Core server implementation containing `processMessage`, session validation, domain routing switches, and the `Command` struct definition.
- **`src/cdp/testing.zig`**: Test harness providing mock clients and assertion helpers for unit testing CDP commands.
- **`src/cdp/id.zig`**: ID generators for targets, sessions, and browser contexts ensuring unique identifiers across the dispatch system.
- **`src/cdp/domains/page.zig`**: Implementation of page navigation and frame management commands.
- **`src/cdp/domains/network.zig`**: Request interception and network event emission logic.
- **`src/cdp/domains/runtime.zig`**: JavaScript evaluation and console API handling.
- **`src/cdp/domains/target.zig`**: Target creation, attachment, and disposal management.

## Summary

- Lightpanda's CDP command dispatch system uses a four-stage pipeline: JSON parsing, session validation, domain routing, and response serialization.
- The **STARTUP** pseudo-session enables early-phase commands before `BrowserContext` initialization, while `isValidSessionId` validates active sessions.
- **Zero-cost routing** is achieved through compile-time bit-casting of domain strings to integers, eliminating runtime hashing via nested switch statements on domain length and value.
- Domain handlers in `src/cdp/domains/*.zig` implement a uniform `processMessage` interface receiving a generic `Command` object for type-safe response generation.
- The `Command` struct provides `sendResult`, `sendError`, and `sendEvent` helpers that abstract JSON-RPC serialization details from domain logic.

## Frequently Asked Questions

### How does Lightpanda handle invalid session IDs in CDP commands?

When a command includes a `sessionId`, Lightpanda validates it against active browser contexts via `isValidSessionId`. If validation fails, the system immediately returns a JSON-RPC error with code `-32001` and the message "Unknown sessionId" through `command.sendError`, preventing unauthorized access to browser contexts.

### What makes Lightpanda's domain routing faster than traditional string matching?

The dispatch system in `src/cdp/cdp.zig` uses compile-time integer bit-casting via `@as(u16, @bitCast(domain[0..2].*))` and similar fixed-width conversions. This technique allows the compiler to resolve domain routing through integer switch statements rather than runtime string comparisons or hash map lookups, resulting in zero-cost routing overhead.

### Can custom CDP domains be added to Lightpanda?

Yes, developers can add custom domains by creating a new Zig file in `src/cdp/domains/` that exports a `processMessage` function accepting a generic `Command` parameter. The domain name must be added to the length-based switch statement in `src/cdp/cdp.zig` using the `asUint` helper to enable compile-time routing.

### How does the STARTUP session differ from regular browser sessions?

The **STARTUP** session is a special string identifier used before any `BrowserContext` exists, allowing early commands like `Page.getFrameTree` during initialization. Unlike regular sessions validated against `BrowserContext.session_id`, STARTUP bypasses normal validation and triggers `dispatchStartupCommand` logic for handling pre-context operations.