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 in browser-runtime.ts
  • Session staleness (beyond 2-second SESSION_TTL_MS) causes sporadic failures—always verify state.sessionAt
  • Use timeout detectors (isInputDispatchTimeout, isKeyboardDispatchTimeout) to implement targeted retries
  • Enable EGO_DEBUG=runtime to expose internal error handling and pending request state
  • Mass promise rejections from handleSendError signal 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:

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 →