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

> Discover how Ego-Lite's browser runtime buffers Chrome DevTools Protocol CDP events using a circular buffer. Learn about its efficient memory management for long sessions.

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

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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:

```ts
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`](https://github.com/citrolabs/ego-lite/blob/main/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:

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

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

```ts
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

```ts
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

```ts
// 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

```ts
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) | High-level API: `waitForBrowserEvent()`, `subscribeBrowserEvent()` |
| [`package/ego-browser/src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts), selective filtering applies buffer policy, and [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) with public API in [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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.