# How ego-browser Manages CDP Sessions and the 2-Second TTL Behavior in ego-lite

> Discover how ego-browser manages CDP sessions with a 2-second TTL in ego-lite. Learn how it reattaches to active tabs for optimal performance and resource management.

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

---

**ego-browser caches Chrome DevTools Protocol sessions for exactly 2 seconds to balance performance and resource hygiene, automatically reattaching to the active tab when the TTL expires or the session becomes invalid.**

In the **citrolabs/ego-lite** runtime, the ego-browser package provides a streamlined bridge to Chrome's DevTools Protocol. Rather than holding permanent CDP connections, it implements a lightweight session manager with deterministic expiration. This design ensures rapid consecutive calls reuse connections while preventing stale sessions from accumulating during long-running automation tasks.

## CDP Session Architecture in ego-browser

All CDP communication flows through [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), which wraps the host runtime's `ego.sendCDPMessage` capability. The session layer maintains minimal state in a centralized store defined in [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts).

The core abstraction is **`ensureSession()`** — an async function that returns a valid session identifier for the current page target. When application code calls high-level helpers like `cdp()` or `js()`, these invoke `browserCdp`, which in turn relies on `ensureSession` to obtain (or reuse) an attachment.

### Session State Structure

The runtime tracks these key fields in `state`:

| Field | Purpose |
|-------|---------|
| `sessionId` | The cached CDP session identifier from the last `Target.attachToTarget` |
| `sessionAt` | Timestamp (ms) when the current session was established |
| `sessionInflight` | Promise for an ongoing attach operation, shared by concurrent callers |
| `sessionTargetId` | The target ID this session is bound to |

Two constants govern behavior:

```ts
const SESSION_TTL_MS = 2000;      // 2-second session lifetime
const MAX_BUFFERED_EVENTS = 10000; // Per-session event buffer cap

```

## The ensureSession Flow and 2s TTL Logic

The `ensureSession` function in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) implements a three-branch decision tree:

```ts
export async function ensureSession() {
  // Branch 1: Re-use cached session if still fresh
  if (state.sessionId && Date.now() - state.sessionAt < SESSION_TTL_MS) {
    return state.sessionId;
  }
  
  // Branch 2: Wait for in-flight attach if another call started one
  if (state.sessionInflight) {
    return state.sessionInflight;
  }
  
  // Branch 3: Start new attach flow
  // ... (see below)
}

```

### TTL Check: The 2-Second Window

The **2-second TTL** is enforced by the first branch. When `Date.now() - state.sessionAt < 2000` evaluates true, the cached `sessionId` returns immediately with no network round-trip. This threshold was chosen to:

- **Batch nearby operations** — Sequential helper calls in tight loops (DOM queries, expression evaluations) share one session
- **Expire before staleness** — Navigation, reloads, or tab changes typically complete within seconds, so old sessions don't linger
- **Limit memory growth** — Per-session event buffers and tracking structures are periodically released

### New Session Attachment

When the TTL expires or no session exists, `ensureSession` executes the full attach sequence:

1. **Enumerate targets** — `browserEgo().listTabs()` retrieves available tabs
2. **Select target** — Uses `state.preferredTargetId` if set, otherwise the first active tab
3. **Attach via CDP** — `rawCdp("Target.attachToTarget", { targetId }, "")` creates the session
4. **Enable page events** — `enablePageEvents(sessionId)` calls `Page.enable` once per session
5. **Record timestamp** — `state.sessionAt = Date.now()` starts the 2-second freshness window

```ts
// Simplified attach sequence from browser-runtime.ts
state.sessionInflight = (async () => {
  const tabs = await browserEgo().listTabs();
  const target = findPreferredTarget(tabs);
  
  const { sessionId } = await rawCdp(
    "Target.attachToTarget",
    { targetId: target.targetId, flatten: true },
    ""  // empty sessionId = send to browser-wide target
  );
  
  await enablePageEvents(sessionId);
  state.sessionId = sessionId;
  state.sessionTargetId = target.targetId;
  state.sessionAt = Date.now();
  return sessionId;
})();

```

## Automatic Session Invalidation

Sessions clear automatically in two scenarios, both routing through `invalidateSession()`:

### Error-Driven Invalidation

When `browserCdp` catches a CDP error matching the `SESSION_LOST` regex (e.g., `"Session with given id not found"`, `"Target closed"`), it calls `invalidateSession()` and retries the original request once with a fresh session:

```ts
// Lines 97-102 in browser-runtime.ts
try {
  return await rawCdp(method, params, sessionId);
} catch (err) {
  if (SESSION_LOST.test(err.message)) {
    invalidateSession();
    // Retry with new session...
  }
  throw err;
}

```

### Event-Driven Invalidation

The `handleMessage` function monitors incoming CDP events. Detection of `Target.detachedFromTarget` or `Target.targetDestroyed` for the current session triggers immediate cleanup:

```ts
// Lines 52-65 in browser-runtime.ts
if (method === "Target.detachedFromTarget" || method === "Target.targetDestroyed") {
  if (params.sessionId === state.sessionId || params.targetId === state.sessionTargetId) {
    invalidateSession();
  }
}

```

### What invalidateSession Does

```ts
function invalidateSession() {
  if (state.sessionId) {
    pageEnabledSessions.delete(state.sessionId);
    // Clear any pending dialog state...
  }
  state.sessionId = "";
  state.sessionTargetId = "";
  state.sessionAt = 0;
}

```

## Practical Usage Patterns

### Automatic Session Management (Default)

Most code uses the `cdp()` helper without thinking about sessions:

```ts
import { cdp } from "ego-browser";

// First call: attaches new session (2s TTL starts)
await cdp("Runtime.evaluate", { expression: "document.title" });

// Second call within 2s: reuses same session
await cdp("DOM.querySelector", { nodeId: 1, selector: "h1" });

// Call after 2s: transparently re-attaches
await new Promise(r => setTimeout(r, 2500));
await cdp("Runtime.evaluate", { expression: "location.href" });

```

### Manual Session Reuse for Batches

For explicit control, import `ensureSession` and `rawCdp` directly:

```ts
import { ensureSession, rawCdp } from "./browser-runtime.js";

// Obtain session explicitly
const session = await ensureSession();

// All calls within TTL share this session ID
await rawCdp("Runtime.evaluate", { expression: "window.scrollY" }, session);
await rawCdp("Runtime.evaluate", { expression: "document.readyState" }, session);
await rawCdp("Page.captureScreenshot", { format: "png" }, session);

```

### Handling Session Loss

The runtime handles most session failures automatically. Application-level error handling catches only unrecoverable errors:

```ts
try {
  // May internally retry once if session lost
  await cdp("Page.printToPDF", { printBackground: true });
} catch (err) {
  // Reached only if retry also failed or error unrelated to session
  console.error("Unrecoverable CDP error:", err);
}

```

## Key Implementation Files

| File | Role |
|------|------|
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | Core transport, `ensureSession()`, TTL logic, invalidation, event routing |
| [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) | Mutable runtime state (`sessionId`, `sessionAt`, `sessionInflight`, etc.) |
| [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts) | Public API surface (`cdp()`, `js()`) that delegates to session layer |

The complete source for these files is available in the [citrolabs/ego-lite](https://github.com/citrolabs/ego-lite) repository under `package/ego-browser/`.

## Summary

- **CDP sessions in ego-browser live for exactly 2 seconds** (`SESSION_TTL_MS = 2000`), balancing reuse against freshness
- **`ensureSession()`** in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) manages acquisition, caching, and transparent re-attachment
- **Invalidation occurs automatically** on session errors or target destruction events, with one automatic retry
- **The design hides complexity** — most callers use `cdp()` without managing session state, yet can opt into explicit control when needed

## Frequently Asked Questions

### What happens if I make CDP calls faster than every 2 seconds?

The cached session ID is reused with zero additional overhead. Only the first call in a sequence triggers `Target.attachToTarget` and `Page.enable`; subsequent calls within the 2-second window skip directly to the requested method.

### Why 2 seconds specifically and not longer?

A 2-second TTL was chosen as a pragmatic middle ground. It is long enough to batch rapid-fire operations (DOM queries, evaluations, screenshots in sequence) yet short enough that navigation, tab reloads, or page transitions typically occur outside the window, ensuring fresh context attachment.

### Does the TTL apply per-tab or globally?

Per the current `state` structure, there is **one cached session globally**. If your automation switches between tabs rapidly, each switch triggers a new `Target.attachToTarget` because the preferred target changes. The TTL applies to whatever session was most recently established, regardless of which tab it serves.

### Can I disable the TTL and hold a permanent session?

Not through the public API. The `ensureSession` function hardcodes `SESSION_TTL_MS` and `invalidateSession` is called automatically on errors. For extended session needs, you would need to maintain a reference to a session ID and pass it explicitly to `rawCdp`, though this bypasses the runtime's stale-session protections.