How ego-browser Manages CDP Sessions and the 2-Second TTL Behavior in ego-lite
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, which wraps the host runtime's ego.sendCDPMessage capability. The session layer maintains minimal state in a centralized store defined in 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:
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 implements a three-branch decision tree:
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:
- Enumerate targets —
browserEgo().listTabs()retrieves available tabs - Select target — Uses
state.preferredTargetIdif set, otherwise the first active tab - Attach via CDP —
rawCdp("Target.attachToTarget", { targetId }, "")creates the session - Enable page events —
enablePageEvents(sessionId)callsPage.enableonce per session - Record timestamp —
state.sessionAt = Date.now()starts the 2-second freshness window
// 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:
// 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:
// 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
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:
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:
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:
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 |
Core transport, ensureSession(), TTL logic, invalidation, event routing |
src/state.ts |
Mutable runtime state (sessionId, sessionAt, sessionInflight, etc.) |
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 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()inbrowser-runtime.tsmanages 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →