How to Debug CDP Timeout Errors in Ego-Browser Automation Scripts
Ego-browser automation scripts fail with "CDP request timed out" when Chromium's DevTools Protocol channel stalls, the target tab closes, or a stale session ID is used; resolve by inspecting session health, enabling runtime logging, and implementing targeted retry logic.
Ego-browser automates Chromium through the Chrome DevTools Protocol (CDP), wrapping every call in a runtime layer that adds 15-second timeouts, session management, and error recovery. When you encounter "CDP request timed out" failures, the root cause usually lies in this transport layer—either the browser process is unresponsive, the target session vanished, or your script is using an expired session ID. This guide walks through the exact debugging workflow using the internal APIs exposed in browser-runtime.ts.
How CDP Calls Flow Through the Runtime
Understanding the request path is essential for tracing where timeouts originate.
The Raw CDP Transport
The rawCdp() function in browser-runtime.ts (lines 55-59) builds JSON payloads, registers pending promises, and sends messages via globalThis.ego.sendCDPMessage. A timer with RESPONSE_TIMEOUT_MS = 15000 rejects the promise if no response arrives:
// browser-runtime.ts#L55-L59 — simplified structure
function rawCdp(method: string, params: object): Promise<any> {
const id = messageId++;
const promise = createPendingPromise(id);
startTimeoutTimer(id, RESPONSE_TIMEOUT_MS); // 15s default
globalThis.ego.sendCDPMessage({ id, method, params });
return promise;
}
Session-Aware Wrapper
browserCdp() (lines 78-104) adds ensureSession() injection and a retry path for lost sessions. It calls rawCdp() and catches timeout errors, attempting recovery for session-related failures.
Session Lifecycle
ensureSession() (lines 107-144) creates or re-attaches CDP sessions, enables Page events, and caches sessions for 2 seconds (SESSION_TTL_MS). If a session is lost, invalidateSession() clears caches. Stale cache entries are a common source of sporadic timeouts.
Where Timeout Errors Surface
Driver helpers forward raw errors with specific detection functions:
| Driver | CDP Method | Timeout Detector | Source Location |
|---|---|---|---|
| Pointer (mouse actions) | Input.dispatchMouseEvent |
isInputDispatchTimeout(error) |
pointer.ts lines 97-100 |
| Keyboard (key presses) | Input.dispatchKeyEvent |
isKeyboardDispatchTimeout(error) |
keyboard.ts lines ~660-663 |
These detectors let you distinguish CDP timeouts from other failures like "selector not found."
Common Causes of CDP Timeouts
| Cause | Symptoms | Mechanism |
|---|---|---|
| Target tab closed/detached | Immediate timeout followed by "Session not found" logs | Response never arrives; fallback retry only works for non-browser-level methods |
| Heavy page load / long script | Timeout after full 15s despite open tab | CDP channel blocked by browser main thread |
| Stale session ID (post-TTL) | Sporadic timeouts after navigation or short waits | SESSION_TTL_MS (2s) expired; manual session ID passing bypasses ensureSession() |
| Browser-level commands | Timeouts on Target.* methods |
browserCdp() skips session injection, leaving only raw timeout protection |
Step-by-Step Debugging Workflow
1. Identify the Failing Method
Read the error message carefully. CDP request timed out: Input.dispatchMouseEvent indicates the pointer driver invoked the call. This determines which timeout detector to use in your retry logic.
2. Verify Session Health
Log session state before critical operations:
import { ensureSession, state } from "./state.js";
async function debugSession() {
await ensureSession();
console.log({
sessionId: state.sessionId,
createdAt: new Date(state.sessionAt).toISOString(),
ttlRemaining: 2000 - (Date.now() - state.sessionAt) // SESSION_TTL_MS = 2000
});
}
Check state.ts for the full structure of stored timestamps and session metadata.
3. Adjust Timeout Durations
Locate driver-specific constants like INPUT_DISPATCH_TIMEOUT_MS. Increase cautiously—higher values mask slow pages but may hide real problems:
// Example: overriding pointer timeout (check driver implementation)
import { INPUT_DISPATCH_TIMEOUT_MS } from "./driver/pointer.js";
// INPUT_DISPATCH_TIMEOUT_MS = 30000; // if mutable, or patch source
4. Enable Runtime Logging
Set EGO_DEBUG=runtime or use the --doctor CLI flag to surface handleSendError invocations and pending request dumps:
EGO_DEBUG=runtime node dist/out/index.js your-script.js
This activates logging at browser-runtime.ts lines 218-231, showing when requests are cleared and errors propagate.
5. Check for Event Backpressure
Call drainBrowserEvents() (lines 64-68) to inspect if events are backing up, indicating downstream processing bottlenecks:
import { drainBrowserEvents } from "./browser-runtime.js";
const buffered = await drainBrowserEvents();
console.log(`Drained ${buffered.length} events`);
6. Implement Targeted Retry Logic
Wrap calls using the appropriate timeout detector:
import { browserCdp, isInputDispatchTimeout } from "./browser-runtime.js";
async function safeMouseClick(x: number, y: number, attempt = 1): Promise<void> {
try {
await browserCdp("Input.dispatchMouseEvent", {
type: "mousePressed",
x, y, button: "left", clickCount: 1
});
} catch (err) {
if (isInputDispatchTimeout(err) && attempt < 3) {
await new Promise(r => setTimeout(r, 500 * attempt));
return safeMouseClick(x, y, attempt + 1);
}
throw err;
}
}
For keyboard actions, substitute isKeyboardDispatchTimeout() from keyboard.ts.
7. Validate DOM Attachment
Ensure selectors remain attached before interaction. A detached element causes CDP calls to target a dead session:
await page.waitForSelector("[data-test='submit']", { state: "visible" });
await safeMouseClick(100, 200);
8. Use the E2E Test Harness
Reproduce failures in taskspace-e2e.test.mjs for consistent logging and debugging:
npm test -- src/taskspace-e2e.test.mjs --grep "navigation flow"
Advanced Diagnostic: Detecting Host-Level Failures
If timeouts persist through all above steps, check for catastrophic browser or transport failure. When handleSendError rejects all pending promises simultaneously, look for log messages containing "task inactive" or "host gone"—these indicate failures outside the JavaScript layer in globalThis.ego.sendCDPMessage.
// browser-runtime.ts#L218-L231 — handleSendError clears all pending
function handleSendError(error: Error) {
for (const [id, { reject }] of pendingRequests) {
reject(error); // Mass rejection indicator
}
pendingRequests.clear();
}
Code Reference: Keyboard Action with Full Diagnostics
import { ensureSession, state } from "./state.js";
import { browserCdp, isKeyboardDispatchTimeout, drainBrowserEvents } from "./browser-runtime.js";
async function typeDiagnostic(text: string): Promise<void> {
// Pre-flight checks
const session = await ensureSession();
const events = await drainBrowserEvents();
console.log("[DIAG] Session:", session);
console.log("[DIAG] Session age:", Date.now() - state.sessionAt, "ms");
console.log("[DIAG] Buffered events:", events.length);
for (const char of text) {
try {
await browserCdp("Input.dispatchKeyEvent", {
type: "keyDown",
text: char
});
await browserCdp("Input.dispatchKeyEvent", {
type: "keyUp",
text: char
});
} catch (e) {
if (isKeyboardDispatchTimeout(e)) {
console.error("[DIAG] Keyboard timeout at char:", char);
console.error("[DIAG] Current session state:", {
id: state.sessionId,
age: Date.now() - state.sessionAt
});
throw e;
}
}
}
}
Summary
- CDP timeouts in ego-browser originate from
rawCdp()'s 15-second timer inbrowser-runtime.ts - Session staleness (beyond 2-second
SESSION_TTL_MS) causes sporadic failures—always verifystate.sessionAt - Use timeout detectors (
isInputDispatchTimeout,isKeyboardDispatchTimeout) to implement targeted retries - Enable
EGO_DEBUG=runtimeto expose internal error handling and pending request state - Mass promise rejections from
handleSendErrorsignal host-level failures requiring infrastructure investigation
Frequently Asked Questions
What is the default CDP timeout in ego-browser?
The default timeout is 15,000 milliseconds (15 seconds), defined as RESPONSE_TIMEOUT_MS in browser-runtime.ts. This applies to all CDP calls through rawCdp() unless overridden by driver-specific constants like INPUT_DISPATCH_TIMEOUT_MS.
How do I distinguish a CDP timeout from a selector timeout?
Check the error message and use type guards: CDP timeouts contain "CDP request timed out" and can be detected with isInputDispatchTimeout() or isKeyboardDispatchTimeout(). Selector timeouts typically throw from waitForSelector() with "waiting for selector" messages and do not trigger the CDP-specific detectors.
Why do I get timeouts after page navigation?
Navigation invalidates cached sessions, but SESSION_TTL_MS (2 seconds) may retain stale references. Call ensureSession() explicitly after navigation, or check state.sessionAt to confirm freshness. Sporadic post-navigation timeouts almost always indicate TTL expiration.
Can I disable CDP timeouts entirely?
No—the timeout is hardcoded in rawCdp() at line 59 of browser-runtime.ts. However, you can increase driver-specific timeouts or wrap calls in retry loops that catch timeout errors and re-execute. For debugging only, patch RESPONSE_TIMEOUT_MS in a forked runtime.
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 →