ScreenshotNeo

BlogEngineering

Puppeteer Connection Transport: How Browser Communication Works

Learn how Puppeteer connects to browsers, how WebSocket and pipe transport differ, and how to attach, disconnect, and troubleshoot a custom transport.

By the ScreenshotNeo team4 October 20268 min read

Puppeteer communicates with a browser through a transport, which carries messages between the Puppeteer client and the browser process. The browser protocol defines what those messages mean: Puppeteer uses Chrome DevTools Protocol (CDP) by default when connecting to a browser, while protocol selection can vary by runtime and browser. Transport and protocol are related, but they are not the same thing.

For an existing browser, the usual approach is puppeteer.connect() with its WebSocket endpoint. For a Chrome browser launched by Puppeteer, pipe: true selects pipe communication instead of WebSocket. To implement a custom connection, provide an object matching Puppeteer’s ConnectionTransport contract: send(message), close(), and optional onmessage and onclose callbacks.

1. The connection path: client, transport, and protocol

Think of a Puppeteer connection in three parts:

  1. Puppeteer client: your Node.js code calling browser and page APIs.
  2. Transport: the communication path carrying messages to and from the browser, such as a WebSocket, a pipe, or a custom implementation.
  3. Browser protocol: the rules describing the messages and operations, such as CDP or WebDriver BiDi.

Choosing a transport does not, by itself, mean choosing a protocol. The connection options include a browser URL, a browser WebSocket endpoint, or a custom transport. The documented protocol default depends on how Puppeteer is used: launching Chrome selects CDP, launching Firefox selects WebDriver BiDi, and connecting to a browser defaults to CDP. Check the API version you use if you explicitly need a particular protocol.

2. Connect to an existing browser over WebSocket

Use this when a browser is already running, for example in another process or on a remote machine. Puppeteer needs a reachable browser endpoint and a compatible browser/protocol setup.

Find the WebSocket endpoint

The browser’s /json/version endpoint exposes a webSocketDebuggerUrl. Puppeteer’s browser.wsEndpoint() returns the endpoint for a browser instance it controls. A typical endpoint looks like ws://HOST:PORT/devtools/browser/<id>.

curl http://HOST:PORT/json/version

Read the webSocketDebuggerUrl value from the JSON response and pass it to puppeteer.connect(). Treat the endpoint as a connection credential: do not publish it or expose an unauthenticated debugging port to untrusted networks.

Runnable Node.js example

Install Puppeteer in a Node.js project with npm install puppeteer. Set BROWSER_WS_ENDPOINT to the endpoint obtained above, then run this script:

const puppeteer = require('puppeteer');

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

  const browser = await puppeteer.connect({ browserWSEndpoint });
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
    console.log(await page.title());
  } finally {
    // Detach this Puppeteer client; leave the browser running.
    browser.disconnect();
  }
}

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

For an existing browser, browserURL is another connection option when you know the browser’s host and port. browserWSEndpoint names the full WebSocket endpoint explicitly. Use the latter when the endpoint is already available; use browserURL when connecting by browser URL is more convenient for your setup. See the official Puppeteer.connect() API and browser management guide.

3. WebSocket versus pipe

Choice Typical use Documented constraint
WebSocket endpoint Attach Puppeteer to an existing browser, including a browser in a separate environment. Requires a usable browser endpoint and network/process access.
pipe: true Launch Chrome through Puppeteer and communicate over a pipe. The launch option is Chrome-only and defaults to false.

Some Progressive Web App operations—install, launch, and uninstall—are documented as pipe-only. If you need those operations, account for the requirement when choosing how to launch the browser. The documentation does not establish that pipe is universally faster, more secure, or more reliable than WebSocket, so choose based on whether you are attaching or launching, browser support, required features, and who owns the browser lifecycle.

Launch Chrome with pipe communication

const puppeteer = require('puppeteer');

async function main() {
  const browser = await puppeteer.launch({ pipe: true });
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
    console.log(await page.title());
  } finally {
    await browser.close();
  }
}

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

pipe: true is a launch option, not a way to attach to an arbitrary existing browser endpoint. For a remote or separately started browser, use a WebSocket endpoint unless your integration supplies another supported transport.

4. Implementing a custom ConnectionTransport

Use a custom transport when your environment already provides a communication channel and you need to adapt it to Puppeteer. The public ConnectionTransport surface consists of:

  • send(message): send a message from Puppeteer through the transport.
  • close(): close the transport.
  • onmessage (optional callback): deliver incoming messages to Puppeteer.
  • onclose (optional callback): notify Puppeteer that the transport has closed.

This small interface is an abstraction contract; it does not specify every framing detail or guarantee reconnection, ordering, or multiplexing behavior. Your adapter must follow the expectations of the Puppeteer version and the underlying channel you use. Do not assume that implementing these four members alone defines a complete wire protocol. See the official ConnectionTransport API reference.

In TypeScript, the shape can be expressed as follows. This illustrates the interface only; it is not a complete transport because the underlying channel and its message encoding are application-specific.

import type { ConnectionTransport } from 'puppeteer-core';

class MyTransport implements ConnectionTransport {
  onmessage?: (message: string) => void;
  onclose?: () => void;

  send(message: string): void {
    // Forward message through your channel using its required framing.
    throw new Error('Connect this method to your channel');
  }

  close(): void {
    // Close your channel and invoke onclose when it has closed.
    throw new Error('Connect this method to your channel');
  }
}

Pass an instance using the transport connection option when the adapter is fully implemented:

const browser = await puppeteer.connect({ transport: myTransport });

5. Disconnecting and closing

Choose lifecycle methods according to who owns the browser:

  • browser.disconnect() detaches Puppeteer. The browser and its pages remain running.
  • browser.close() closes the browser.

When attaching to a browser managed elsewhere, disconnect when your work is done so that the owner can keep using it. When your script launched a browser and is responsible for cleanup, close it. The official Browser API documents this distinction.

6. Browser-side Puppeteer and remote browsers

Puppeteer can run in a browser-side environment and connect to a separate browser over WebSocket. That environment cannot launch or download a browser because those operations depend on Node.js APIs. If you need to launch a browser, run the launching code in a Node.js environment; if a browser is already running elsewhere, connect to it using a reachable endpoint.

7. Troubleshooting connection failures

Symptom Likely cause What to check
Connection fails or times out Incorrect endpoint, browser is stopped, or host/port is unreachable. Fetch http://HOST:PORT/json/version from the same environment as Puppeteer; verify the returned endpoint and network access.
WebSocket handshake fails A browser URL was supplied where a WebSocket endpoint was expected, or the endpoint path/ID is stale. Use the full webSocketDebuggerUrl for browserWSEndpoint, or use the appropriate browserURL option.
Browser disconnects during work The remote browser process or its transport ended, or connectivity was interrupted. Check browser process ownership and transport availability. Reconnect only after confirming the browser is still running and obtain its current endpoint.
Browser closes when you expected it to persist The code called browser.close(). Use browser.disconnect() when you only need to detach Puppeteer from a separately managed browser.
Pipe option does not work with the setup pipe is a Chrome launch option, not a general remote-attach mode. Use it when Puppeteer launches Chrome; use a WebSocket endpoint to connect to an existing browser.
Custom transport connects but commands fail The adapter does not match the expected message handling or underlying channel framing. Verify that it forwards messages and close events as required by the Puppeteer version and channel. The interface does not define all framing or protocol behavior.
Launch or download fails in browser-side code Those operations depend on Node.js APIs unavailable in that environment. Launch in Node.js, or connect browser-side Puppeteer to a browser already running elsewhere.

8. Performance, reliability, and cost considerations

The documented API and guide establish transport choices and lifecycle behavior, but do not provide comparative benchmarks for speed, reliability, or cost. There is no universal winner on those measures in the cited documentation. For a reliable integration, make browser ownership explicit, use an endpoint that is reachable from the Puppeteer process, handle connection closure in your application, and avoid assuming that a disconnected client also stopped the browser.

Costs come from the environment hosting the browser and the work your application performs; the transport option itself does not establish a pricing model. For repeated captures, consider whether you need to manage a browser process at all. ScreenshotNeo provides a website screenshot API and MCP server; its plans are Free for 1,000 shots per month, then Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan.

9. Or skip the browser setup

If your goal is a website screenshot rather than browser automation, ScreenshotNeo can return an image or PDF from one request. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month.

10. FAQ

What is Puppeteer’s WebSocket endpoint?

It is the browser’s debugger WebSocket address, commonly shaped like ws://HOST:PORT/devtools/browser/<id>. The /json/version response exposes it as webSocketDebuggerUrl.

Can I use pipe to connect to a browser on another machine?

The documented pipe setting selects communication for Chrome launched through Puppeteer. For an existing remote browser, connect using its WebSocket endpoint.

Does disconnecting stop the browser?

No. browser.disconnect() detaches Puppeteer and leaves the browser and pages running. browser.close() closes the browser.

Is a custom transport a custom browser protocol?

No. A transport adapts the connection path. The browser protocol defines the meaning of the messages.

Official references