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

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.

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.

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).

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.

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:

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:

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:

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →