Ego-Lite Event Buffering Mechanism: How the Browser Runtime Handles CDP Events

Ego-Lite buffers Chrome DevTools Protocol (CDP) events in a fixed-size circular buffer limited to 10,000 entries, using bulk truncation and selective omission to maintain predictable memory usage during long-running browser sessions.

The ego-browser runtime in citrolabs/ego-lite manages a continuous stream of CDP events from the embedded browser. To prevent unbounded memory growth, the runtime implements a bounded buffering strategy that retains only recent events while discarding older ones. This article examines how browser-runtime.ts implements this mechanism, including buffer sizing, truncation policies, and selective filtering.

How the Circular Buffer Works

The buffering system centers on three core components: a fixed capacity limit, an in-memory array, and a bulk truncation policy that runs after every event insertion.

Buffer Size and Storage

The runtime defines an explicit upper bound for retained events:

const MAX_BUFFERED_EVENTS = 10000;   // Upper bound for buffered CDP events

All incoming CDP messages are pushed onto the events array (line 20 in browser-runtime.ts). This array serves as the in-memory circular buffer, growing temporarily until the truncation logic activates.

Truncation Policy

After each push, the runtime enforces the capacity limit by removing oldest entries in bulk:

if (events.length > MAX_BUFFERED_EVENTS) {
  events.splice(0, events.length - MAX_BUFFERED_EVENTS);
}

This bulk splice operation is computationally efficient—it calculates exactly how many elements exceed the limit and removes them in a single array mutation. The result: exactly 10,000 most recent events remain, with earlier events discarded without individual iteration.

Selective Event Buffering in Ego-Lite

Not all CDP events enter the buffer. The runtime applies conditional filtering before the push operation to exclude events that have already been handled.

Excluding Consumed Screencast Frames

The Page.screencastFrame event receives special treatment. When a subscriber has already received and processed this event, it bypasses buffering entirely:

if (!(deliveredToSubscriber && data.method === "Page.screencastFrame")) {
  events.push(data);
  // … truncation logic …
}

This guard prevents redundant storage of visual frame data, which can be large and frequent. The check combines two conditions: whether a subscriber received the event (deliveredToSubscriber) and whether it's a screencast frame (data.method === "Page.screencastFrame"). Both must be true for exclusion.

Why Selective Omission Matters

  • Memory efficiency: Screencast frames contain Base64-encoded image data; skipping them avoids megabytes of duplicate storage
  • Latency reduction: Eliminates unnecessary push and splice operations for high-frequency events
  • Semantic correctness: Events already handled by live subscribers rarely need retrospective batch access

Draining the Event Buffer

Higher-level helpers consume buffered events through a dedicated drainage function. This extracts and clears the entire buffer in one operation:

export function drainBrowserEvents() {
  const out = events.splice(0, events.length);
  return out;
}

The splice(0, events.length) pattern atomically empties the array while returning all contents—no separate clear operation needed. This ensures exactly-once delivery: events move from buffer to caller without risk of double-processing.

Practical Usage Examples

Waiting for Specific CDP Events

import { waitForBrowserEvent, drainBrowserEvents } from "ego-browser";

// Wait for a network request matching a URL pattern
await waitForBrowserEvent(
  (e) => e.method === "Network.requestWillBeSent" 
    && e.params?.request?.url?.includes("api"),
);

The waitForBrowserEvent helper searches both incoming live events and the buffered history, allowing pattern matching across recent activity.

Extracting Buffered Event Batches

// Retrieve current buffer contents (up to 10,000 most recent)
const recentEvents = drainBrowserEvents();
console.log(`Buffered ${recentEvents.length} CDP events`);

Use this for post-hoc analysis, audit logging, or debugging when you need to examine recent browser activity without blocking on live events.

Managing Subscriptions That Affect Buffering

// Subscribe to screencast frames—handled events skip the buffer
const unsubscribe = subscribeBrowserEvent(
  "Page.screencastFrame",
  undefined,
  (frame) => {
    console.log("Got frame", frame.params?.timestamp);
  },
);

// Clean up when finished
unsubscribe();

The subscribeBrowserEvent helper registers a live handler. As implemented in browser-runtime.ts, this flags subsequent matching events with deliveredToSubscriber = true, triggering the exclusion logic that prevents buffer entry.

Key Source Files in Ego-Lite

File Role in Event Buffering
package/ego-browser/src/browser-runtime.ts Core implementation: MAX_BUFFERED_EVENTS constant, events array, drainBrowserEvents() function, truncation and filtering logic
package/ego-browser/src/helpers.ts High-level API: waitForBrowserEvent(), subscribeBrowserEvent()
package/ego-browser/src/state.ts Runtime state including session identifiers used by message routing

These files form a complete pipeline: raw CDP messages enter through browser-runtime.ts, selective filtering applies buffer policy, and helpers.ts exposes consumption patterns for application code.

Performance Characteristics

  • Time complexity: O(1) amortized for event insertion; bulk splice cost scales with overflow amount, not total buffer size
  • Space complexity: O(MAX_BUFFERED_EVENTS) — strictly bounded regardless of session duration
  • Memory predictability: 10,000 events × average event size ≈ known upper bound; no garbage collection pressure from unbounded growth

The bulk truncation approach sacrifices granularity (entire batch removal rather than individual eviction) for implementation simplicity and performance consistency.

Summary

  • Ego-Lite's event buffering uses a 10,000-entry circular buffer with bulk truncation to cap memory usage
  • Selective omission prevents Page.screencastFrame events from entering the buffer when already handled by subscribers
  • drainBrowserEvents() provides atomic extraction and clearing for batch consumption
  • Source location: All logic lives in package/ego-browser/src/browser-runtime.ts with public API in helpers.ts

Frequently Asked Questions

How does ego-lite prevent memory leaks from CDP events?

The runtime enforces a hard limit of 10,000 buffered events through the MAX_BUFFERED_EVENTS constant. When this limit is exceeded, browser-runtime.ts removes the oldest entries in bulk using splice(0, events.length - MAX_BUFFERED_EVENTS). This guarantees bounded memory usage regardless of how long the browser session runs.

Why are some CDP events excluded from the buffer?

Events that have already been delivered to active subscribers—specifically Page.screencastFrame frames—are skipped to avoid storing redundant, potentially large data. The check !(deliveredToSubscriber && data.method === "Page.screencastFrame") guards the push operation, ensuring only unhandled events are retained for later batch access.

What is the difference between waitForBrowserEvent and drainBrowserEvents?

waitForBrowserEvent blocks until a matching event arrives (checking both live stream and buffer), then returns that single event. drainBrowserEvents immediately extracts and clears the entire buffer contents as an array, useful for retrospective inspection or logging. Both are exported from ego-browser but serve different consumption patterns.

Can the buffer size limit be configured?

No—the MAX_BUFFERED_EVENTS = 10000 constant is hardcoded in browser-runtime.ts. Applications requiring different retention policies would need to fork or wrap the drainage function, consuming events more frequently to implement custom windowing behavior.

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 →