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

> Understand how Lightpanda detects network idle states using its 500ms stability mechanism. Learn how zero network activity triggers lifecycle events.

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

---

**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:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
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.