How Lightpanda Detects Network Idle States: The 500ms Stability Mechanism Explained

Lightpanda detects network idle states by combining real-time HTTP transfer counters with a timed state machine that requires zero network activity to persist for at least 500 milliseconds before emitting lifecycle events.

The Lightpanda browser engine, maintained in the lightpanda-io/browser repository, determines when a page reaches network idle (zero active requests) or network almost idle (minimal activity) using a reliability-focused stability window. This detection mechanism is essential for automation protocols like the Chrome DevTools Protocol (CDP) to identify precisely when resource loading has concluded.

Monitoring HTTP Activity Counters

The detection system tracks active network transfers through counters defined in src/browser/HttpClient.zig. This module maintains two critical fields:

  • active: The number of currently executing HTTP transfers.
  • intercepted: The number of requests intercepted by the browser but pending resolution.

After each macro-task execution, src/browser/Session.zig computes the total network activity:

const http_active = http_client.active;
const total_network_activity = http_active + http_client.intercepted;

The idle condition evaluates total_network_activity == 0, while the almost idle threshold triggers when total_network_activity <= 2.

The IdleNotification State Machine

The core timing logic resides in src/browser/Page.zig within the IdleNotification union. This state machine implements a mandatory 500ms stability window to prevent false positives from transient network pauses:

const IdleNotification = union(enum) {
    init,                     // never triggered
    triggered: u64,           // timestamp when condition first became true
    done,                     // notification already sent

    pub fn check(self: *IdleNotification, active: bool) bool {
        if (active) {
            switch (self.*) {
                .done => {},
                .init => {
                    self.* = .{ .triggered = milliTimestamp(.monotonic) };
                },
                .triggered => |ms| {
                    if (milliTimestamp(.monotonic) - ms >= 500) {
                        self.* = .done;
                        return true;
                    }
                },
            }
        } else {
            if (self.* != .done) self.* = .init;
        }
        return false;
    }
};

The check method progresses through three distinct phases:

  1. .init: When the idle condition first becomes true, the state transitions to .triggered and records the current monotonic timestamp.
  2. .triggered: If the condition remains true for 500ms, the state advances to .done and the method returns true.
  3. .done: The notification has been sent. This terminal state never resets, ensuring exactly one notification per page lifecycle.

Emitting Network Idle Events

Once the stability requirement is satisfied, src/browser/Session.zig invokes the page notification methods:

if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
    page.notifyNetworkAlmostIdle();
}
if (page._notified_network_idle.check(total_network_activity == 0)) {
    page.notifyNetworkIdle();
}

The notifyNetworkIdle method in src/browser/Page.zig dispatches the event through the session notification system:

pub fn notifyNetworkIdle(self: *Page) void {
    lp.assert(self._notified_network_idle == .done, "Page.notifyNetworkIdle", .{});
    self._session.notification.dispatch(.page_network_idle, &.{
        .req_id = self._req_id,
        .frame_id = self._frame_id,
        .timestamp = timestamp(.monotonic)
    });
}

For CDP clients, src/cdp/domains/page.zig forwards these events using standard DevTools protocol nomenclature:

if (page._notified_network_idle.check(total_network_activity == 0)) {
    try sendPageLifecycle(bc, "networkIdle", now, frame_id, loader_id);
}

Practical Implementation Examples

When integrating with Lightpanda's notification system declared in src/Notification.zig, register a listener to react when the 500ms idle window completes:

try notif.register(.page_network_idle, self, struct {
    pub fn onIdle(event: *Notification.PageNetworkIdle) void {
        std.debug.print(
            "Page {} reached network idle at timestamp {d}\n",
            .{ event.frame_id, event.timestamp },
        );
    }
}.onIdle);

To test the state machine behavior directly, simulate the timing requirements:

var idle_state = IdleNotification.init;
const network_is_idle = true;

// First detection records timestamp, returns false
assert(!idle_state.check(network_is_idle)); // State now: .triggered

// Simulate 600ms elapsed time
std.time.sleep(600 * std.time.ns_per_ms);

// 500ms threshold met, returns true and sets .done
assert(idle_state.check(network_is_idle)); // Notification fired

Summary

Lightpanda's network idle detection implementation ensures reliable automation signals through:

  • Dual counters (active and intercepted) in HttpClient.zig that track all HTTP activity.
  • The IdleNotification state machine in Page.zig enforcing a mandatory 500ms stability period.
  • Exactly-once semantics guaranteeing page_network_idle and page_network_almost_idle events fire only once per page lifecycle.
  • CDP compatibility via src/cdp/domains/page.zig mapping internal states to standard networkIdle lifecycle events.

Frequently Asked Questions

What defines the "network almost idle" state in Lightpanda?

The network almost idle state activates when total_network_activity (the sum of active and intercepted counters from HttpClient.zig) drops to 2 or fewer requests. This threshold, evaluated in src/browser/Session.zig, indicates that primary resource loading has nearly finished while tolerating minor background activity.

Why does Lightpanda require a 500ms window before declaring network idle?

The 500ms stability window prevents false positives from brief network pauses between packet transfers. According to the IdleNotification.check implementation in src/browser/Page.zig, the idle condition must remain continuously true for 500 milliseconds before the state transitions from .triggered to .done and emits the notification.

How does Lightpanda prevent duplicate idle notifications?

The IdleNotification union uses a terminal .done state that persists for the page's lifetime. Once the check method sets this state after the 500ms window elapses, it returns false for all subsequent calls. This ensures that notifyNetworkIdle and notifyNetworkAlmostIdle execute exactly once, as verified by runtime assertions in the source code.

Where are network idle events dispatched in the codebase?

Events dispatch from two locations. Internally, src/browser/Page.zig calls self._session.notification.dispatch() with .page_network_idle or .page_network_almost_idle enum values. For external CDP clients, src/cdp/domains/page.zig invokes sendPageLifecycle() with string identifiers "networkIdle" and "networkAlmostIdle" to maintain Chrome DevTools Protocol compliance.

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 →