ScreenshotNeo

BlogHow-to

How to Send Messages Through Puppeteer Connection Transport

Learn when to use Puppeteer’s ConnectionTransport versus CDPSession.send(), how to connect to a browser, and how to diagnose transport issues.

By the ScreenshotNeo team4 October 20269 min read

Puppeteer’s ConnectionTransport is the low-level interface for passing string messages between Puppeteer and a browser connection. To provide your own connection adapter, implement send(message), close(), and the onmessage and onclose callbacks, then pass it as transport to puppeteer.connect().

If you want to send an ordinary Chrome DevTools Protocol (CDP) command, use CDPSession.send(method, params) instead. It takes a protocol method and parameters and returns a promise for the command result. A transport message is a serialized string; a CDP session call is the command-level API. See the ConnectionTransport reference, CDPSession.send() reference, and ConnectOptions reference.

1. Choose the right API

What you need to do Use What it accepts
Issue a CDP command to a page or target CDPSession.send() A method name, such as Runtime.evaluate, and its parameter object
Supply a custom connection mechanism to Puppeteer ConnectionTransport via puppeteer.connect({transport}) A string message sent over the underlying connection; inbound messages arrive through onmessage
Connect to a browser’s existing WebSocket endpoint puppeteer.connect({browserWSEndpoint}) A browser debugger WebSocket URL

For most code that sends raw CDP commands, you do not need to implement a transport. Puppeteer already provides the connection layer; create a CDP session and call send().

2. Connect to an existing browser and send a CDP command

This Node.js example connects to a running browser using its WebSocket debugger endpoint, opens a page, creates a CDP session, and evaluates a small expression. It requires a Node.js project with Puppeteer installed and a browser already running with remote debugging enabled.

const puppeteer = require('puppeteer');

async function main() {
  const browserWSEndpoint = process.env.PUPPETEER_WS_ENDPOINT;
  if (!browserWSEndpoint) {
    throw new Error('Set PUPPETEER_WS_ENDPOINT to the browser WebSocket endpoint');
  }

  let browser;
  try {
    browser = await puppeteer.connect({ browserWSEndpoint });
    const page = await browser.newPage();
    const session = await page.createCDPSession();

    const result = await session.send('Runtime.evaluate', {
      expression: '1 + 1',
      returnByValue: true,
    });
    console.log(result.result.value); // 2

    await session.detach();
    await page.close();
  } finally {
    // disconnect() closes Puppeteer's connection without shutting down
    // the browser process it connected to.
    if (browser) browser.disconnect();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The browser WebSocket endpoint can be obtained from browser.wsEndpoint() when you control the browser instance that was launched, or from the webSocketDebuggerUrl field at http://HOST:PORT/json/version when the browser exposes that debugging endpoint. Puppeteer documents the endpoint format as ws://HOST:PORT/devtools/browser/<id>. Protect the debugging endpoint: anyone who can reach it may be able to control the browser. See the Browser.wsEndpoint() reference and connect() reference.

Send another CDP method

Replace Runtime.evaluate and its parameter object with a CDP command supported by the browser you connected to. The method name and parameter names must match the protocol version on that browser. For example, to read the current document title, evaluate JavaScript in the page target:

const title = await session.send('Runtime.evaluate', {
  expression: 'document.title',
  returnByValue: true,
});
console.log(title.result.value);

The exact result shape depends on the command. Consult the protocol definition for the browser’s version rather than assuming every CDP method returns a value in the same shape.

3. Implement a custom ConnectionTransport

A custom transport is useful when the browser connection must pass through an adapter you control, such as a specialized WebSocket wrapper. The documented interface describes the boundary but does not prescribe a complete implementation, reconnection policy, buffering, backpressure, or error propagation. The following example demonstrates the interface with a Node.js WebSocket connection; confirm the interface against the Puppeteer release installed in your project.

Install the dependencies with npm install puppeteer ws. Set PUPPETEER_WS_ENDPOINT to a valid browser WebSocket URL.

const puppeteer = require('puppeteer');
const WebSocket = require('ws');

class WebSocketTransport {
  constructor(endpoint) {
    this.ws = new WebSocket(endpoint);
    this.onmessage = undefined;
    this.onclose = undefined;

    this.ws.on('message', (data) => {
      // Puppeteer's transport interface delivers strings.
      const message = data.toString();
      this.onmessage?.(message);
    });
    this.ws.on('close', () => {
      this.onclose?.();
    });
    this.ws.on('error', (error) => {
      // The public transport shape cited here does not define an error
      // callback. Log or route errors through your own adapter policy.
      console.error('Transport WebSocket error:', error);
    });
  }

  send(message) {
    if (this.ws.readyState !== WebSocket.OPEN) {
      throw new Error('Transport WebSocket is not open');
    }
    this.ws.send(message);
  }

  close() {
    this.ws.close();
  }
}

async function main() {
  const endpoint = process.env.PUPPETEER_WS_ENDPOINT;
  if (!endpoint) throw new Error('Set PUPPETEER_WS_ENDPOINT');

  const transport = new WebSocketTransport(endpoint);
  // Wait until the underlying WebSocket is ready before connecting.
  await new Promise((resolve, reject) => {
    transport.ws.once('open', resolve);
    transport.ws.once('error', reject);
  });

  let browser;
  try {
    browser = await puppeteer.connect({ transport });
    const page = await browser.newPage();
    const session = await page.createCDPSession();
    const result = await session.send('Runtime.evaluate', {
      expression: '1 + 1',
      returnByValue: true,
    });
    console.log(result.result.value);
    await session.detach();
    await page.close();
  } finally {
    if (browser) browser.disconnect();
    else transport.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The sample shows the interface shape: Puppeteer calls send(string); the adapter passes browser messages to the assigned onmessage callback and calls onclose when the socket closes. It does not promise that the sample’s event handling covers every WebSocket implementation or Puppeteer release. Keep one owner for closing the underlying connection, avoid silently dropping messages, and define how your application handles socket errors and unexpected closure.

4. Configure the connection

puppeteer.connect() accepts ConnectOptions. For this task, the key choices are the browser endpoint or custom transport and the protocol and timeout settings relevant to the browser you are connecting to.

Option or value Purpose Practical note
browserWSEndpoint Connect using the browser’s WebSocket debugger endpoint Use the endpoint from browser.wsEndpoint() or the browser’s /json/version response.
transport Provide a custom ConnectionTransport Use when the connection mechanism itself needs adapting. The documented option is the adapter entry point.
protocol Choose the automation protocol The current reference says CDP is the default when connecting to a browser. Check your installed version’s reference if changing it.
protocolTimeout Set the timeout for individual CDP calls The referenced API documents a default of 180,000 ms. A longer timeout can allow slow commands more time, but does not fix a stalled connection.

The current official API pages in the research dossier display Puppeteer 25.12.0. Options and defaults can change, so check the documentation for the version in your lockfile before relying on a default or copying configuration into production. The reference also documents CDP as the default when launching Chrome and WebDriver BiDi when launching Firefox; this article’s examples connect to an existing browser.

5. cURL, Python, and Node.js examples for ScreenshotNeo

If the goal is to obtain a page screenshot rather than issue arbitrary CDP commands or build a browser transport, ScreenshotNeo provides a screenshot API: one GET request with a URL returns an image or PDF. Its website describes the service, and the API documentation lists the request options.

cURL:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.write(r.content)

Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);

Or skip the browser setup

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, with the page verdict and billed status reported in response headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

See the ScreenshotNeo API docs for options, then sign up for 1,000 free screenshots a month with no card.

6. Troubleshooting

Symptom Likely cause What to check
puppeteer.connect() cannot connect Wrong endpoint, browser not listening, or network path blocked Verify the full WebSocket URL and that the browser debugging endpoint is reachable from the process running Puppeteer. Check /json/version where available.
WebSocket closes during setup Connection was closed before or during Puppeteer initialization Check browser logs, endpoint access, and whether another component owns or closes the socket. A custom adapter should notify Puppeteer through onclose.
CDP command rejects with an unknown method or invalid parameters Method or parameter names do not match the browser’s protocol version Check the protocol schema for the browser actually running; do not assume the local Puppeteer version controls a remote browser’s capabilities.
Command times out The command is slow, target is stalled, or connection has stopped progressing Check page and browser responsiveness. Adjust protocolTimeout only when the operation legitimately needs more time; a timeout increase cannot repair a broken socket.
Transport says the socket is not open The adapter sent before the WebSocket opened or after it closed Wait for its open event before passing it to Puppeteer, and reject or handle sends after closure according to the adapter’s policy.
Messages appear to disappear in a custom adapter The adapter drops, reorders, or mishandles WebSocket frames Forward each inbound message as a string to onmessage and ensure outgoing messages are not silently discarded. The public interface documentation does not specify buffering or backpressure guarantees.
Browser build cannot launch or download Chrome Browser runtime lacks Node.js APIs used for launching and downloading Use the browser-compatible build to connect to an already running browser. The Puppeteer browser guide describes launch and download as unsupported in that runtime.

7. Performance, reliability, and cost

  • Use the command API when possible. CDPSession.send() avoids owning the low-level socket adapter and its lifecycle.
  • Keep the connection local to the work that needs it. Reuse an established browser connection where appropriate rather than repeatedly connecting for each command; manage page and session cleanup explicitly.
  • Treat transport behavior as your responsibility. The API reference defines callbacks and methods but does not establish reconnection, retry, buffering, or backpressure behavior. Do not automatically replay commands after reconnect unless your application knows that repeating the operation is safe.
  • Size timeouts to the operation. The documented default for individual CDP calls is 180 seconds in the referenced API version. Longer waits consume worker time; shorter values can fail legitimate slow operations.
  • Account for browser runtime constraints. In-browser Puppeteer can connect to an existing browser over WebSocket, but launching or downloading the browser is not supported there.
  • Cost depends on how you run the browser. Puppeteer’s connection API has no price stated in the cited references; hosting and operating the remote browser are separate from Puppeteer itself. If you only need page images or PDFs, ScreenshotNeo’s published plans are free for 1,000 shots per month, then $5 for 3,000, $15 for 15,000, $39 for 60,000, $99 for 250,000, or $249 for 1,000,000. Yearly billing gives two months free; every feature is on every plan.

8. FAQ

Does ConnectionTransport.send() accept a CDP method name?

No. It accepts a message string for the transport layer. Use CDPSession.send(method, params) to invoke a CDP command by method name.

Can I use a custom transport and browserWSEndpoint together?

The documented connection options expose both as ways to provide connection details. Use the option that matches your connection setup and check the exact installed Puppeteer version’s reference for option interaction details.

Can Puppeteer reconnect automatically if the transport closes?

The cited interface reference does not promise automatic reconnection. If you need recovery, implement and validate that behavior in the layer that owns your transport and browser lifecycle.

Is a custom transport required to send raw CDP commands?

No. Connect normally, create a CDP session for the relevant target, and call session.send().

References