# How Ego-Lite Buffers and Drains CDP Events: Inside the Browser Runtime

> Discover how ego-lite buffers and drains CDP events within its browser runtime. Learn about the in-memory queue and efficient event handling for real-time matches.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-27

---

**Ego-Lite maintains a bounded in-memory queue of up to 10,000 Chrome DevTools Protocol events in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), draining them atomically via `drainBrowserEvents()` while routing real-time matches to subscribers and predicate-based waiters.**

The citrolabs/ego-lite repository provides a lightweight automation layer for Chrome DevTools Protocol (CDP) that must handle asynchronous browser events without data loss. Understanding how ego-lite buffers and drains CDP events is essential for building reliable automation scripts that capture every critical `Page.lifecycleEvent` or `Runtime.consoleAPICalled` signal.

## The Bounded CDP Event Buffer

### Buffer Configuration and Limits

At the core of the buffering system lies a simple but effective bounded array. According to the ego-lite source code in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), the runtime defines `MAX_BUFFERED_EVENTS = 10000` around lines 6-9 to prevent unbounded memory growth. The system initializes three key data structures around lines 20-22: a mutable `events = []` array that stores buffered events, an `eventSubscribers` registry for real-time listeners, and an `eventWaiters` list for promise-based predicates.

### Message Classification in handleMessage()

When Chrome sends a JSON message over the CDP socket, the `handleMessage()` function (lines 31-45) immediately parses the payload. The function distinguishes between command responses and events by checking for the presence of an `id` field; messages containing an `id` are routed to pending request handlers, while those without an `id` are processed as generic CDP events subject to buffering logic.

### Subscriber Routing and Fallback Buffering

For each incoming event, the runtime iterates over `eventSubscribers` (lines 77-84). If a subscriber’s `method` string matches the event’s `data.method` and the optional `sessionId` aligns, the listener is invoked immediately. However, if no subscriber consumes the event—or if the event is not a `Page.screencastFrame` that has already been processed—the event is pushed onto the `events` array (lines 85-89). This dual-path design ensures that transient events are captured even if no listener is currently registered.

### Enforcing the 10K Event Limit

To prevent memory leaks during long-running automation sessions, the runtime enforces its size limit immediately after every push. Around lines 90-91, the code executes `events.splice(0, events.length - MAX_BUFFERED_EVENTS)`, which removes the oldest excess entries from the front of the array. This guarantees that the buffer never grows beyond the configured 10,000-event threshold, regardless of event velocity.

## Draining Mechanisms and Event Retrieval

### Atomic Buffer Drain with drainBrowserEvents()

The public API `drainBrowserEvents()` provides an atomic snapshot of all buffered events. As implemented in lines 64-66, the function executes `events.splice(0, events.length)`, which removes every item from the internal array in a single operation and returns them as a new array to the caller. This approach ensures that no events are lost between the read and clear operations, even under high concurrency.

### Predicate-Based Waiting with waitForBrowserEvent()

For scenarios requiring specific events rather than bulk retrieval, `waitForBrowserEvent()` registers a predicate-based waiter. When a new event arrives, the runtime evaluates it against all registered `eventWaiters` (lines 73-84). If a waiter’s predicate function returns true, the promise resolves immediately with that event, and the waiter is removed from the list. Events that do not satisfy any waiter predicates remain in the buffer for later draining.

### Real-Time Subscriptions via subscribeBrowserEvent()

In addition to buffering, the runtime supports persistent subscriptions through `subscribeBrowserEvent()`, exposed via [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts). Subscribers registered with this API receive events in real-time as they arrive, bypassing the buffer entirely. Only events that fail to match any subscriber fall back to the buffered `events` array, creating a tiered consumption model that prioritizes immediate handling while preserving data for later inspection.

## Implementation Examples

### Draining All Pending CDP Events

To retrieve and clear all buffered events from a running session:

```typescript
import { drainBrowserEvents } from "ego-browser/src/browser-runtime.js";

async function logPendingEvents() {
  const pending = drainBrowserEvents();   // empties the buffer atomically
  console.log("Pending CDP events:", pending);
}

```

This call returns an array of all events buffered since the last drain, including any `Target.attachedToTarget` or `Network.responseReceived` messages that were not consumed by subscribers.

### Waiting for Specific Navigation Events

To block execution until a specific lifecycle event occurs:

```typescript
import { waitForBrowserEvent } from "ego-browser/src/browser-runtime.js";

async function waitForNavigation() {
  const navigationEvent = await waitForBrowserEvent(
    (e) => e.method === "Page.lifecycleEvent" && e.params?.name === "load"
  );
  console.log("Page loaded:", navigationEvent);
}

```

The predicate resolves the promise the moment a matching `Page.lifecycleEvent` appears, checking both new incoming events and those already present in the buffer.

### Subscribing to Real-Time Console Output

To capture console messages continuously without buffering delays:

```typescript
import { subscribeBrowserEvent } from "ego-browser/src/browser-runtime.js";

const unsubscribe = subscribeBrowserEvent(
  "Runtime.consoleAPICalled",
  undefined,        // listen on any session
  (event) => console.log("Console:", event.params.args)
);

// Later, when you no longer need the subscription:
unsubscribe();

```

This subscriber receives `Runtime.consoleAPICalled` events immediately as they arrive from Chrome, preventing them from ever reaching the bounded buffer.

## Summary

- **Bounded memory safety**: The 10,000-event hard limit in `MAX_BUFFERED_EVENTS` prevents memory leaks via automatic truncation of the oldest entries.
- **Atomic draining**: `drainBrowserEvents()` uses a single `splice` operation to guarantee no event loss during retrieval.
- **Tiered consumption**: The system prioritizes real-time subscribers and predicate waiters, falling back to the buffer only for unconsumed events.
- **Flexible retrieval**: Developers can choose between bulk draining, specific waiting, or continuous subscription based on their automation needs.

## Frequently Asked Questions

### What happens when the CDP event buffer reaches 10,000 entries?

When the events array exceeds `MAX_BUFFERED_EVENTS`, the runtime removes the oldest excess entries using `events.splice(0, events.length - MAX_BUFFERED_EVENTS)`. This ensures the buffer never grows unboundedly, protecting against memory exhaustion during long-running browser automation sessions.

### How does ego-lite distinguish between CDP command responses and events?

The `handleMessage()` function checks for the presence of an `id` field in the incoming JSON. Messages containing an `id` are treated as responses to pending CDP commands and routed to their respective request handlers, while messages lacking an `id` are processed as asynchronous events subject to buffering and subscriber routing.

### What is the difference between draining and waiting for events?

Draining via `drainBrowserEvents()` immediately empties the entire buffer and returns all captured events as an array, clearing the internal state. In contrast, `waitForBrowserEvent()` registers a predicate function that resolves when a specific event arrives, without necessarily removing other events from the buffer or altering the queue state beyond the matching entry.

### Can multiple subscribers receive the same CDP event simultaneously?

Yes, the runtime iterates over all `eventSubscribers` and invokes every listener whose `method` and optional `sessionId` match the incoming event. If no subscribers match, the event falls back to the buffer; there is no exclusive consumption, allowing multiple automation components to react to the same browser signal independently.