For AI agents: a documentation index is available at /llms.txt
Skip to main content

Connect Puppeteer

Run your Puppeteer scripts against managed browsers by replacing puppeteer.launch() with puppeteer.connect(). Everything after the connection line, including selectors, waits, and evaluation, stays exactly as you wrote it.

This page is for code you already have. If you're writing automation from scratch, the BAP SDK is usually the better starting point.

Steps

  1. Get Your API Token

    Sign up for a free account, then copy your API token from the account dashboard. Every connection carries it as a ?token= query parameter.

  2. Install puppeteer-core

    npm install puppeteer-core

    Use puppeteer-core rather than puppeteer. The full package downloads a Chromium binary at install time, and you won't be running it: the browser lives on Browserless. The two packages expose the same API, so imports and method calls don't change.

  3. Swap launch() for connect()

    import puppeteer from "puppeteer-core";

    const TOKEN = "YOUR_API_TOKEN_HERE";

    // Before: a browser process on this machine.
    // const browser = await puppeteer.launch();

    // After: a managed browser reached over a CDP WebSocket.
    const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
    });

    The token goes in the query string. wss:// is required, since the connection is a WebSocket rather than an HTTP request.

  4. Run a Complete Script

    import puppeteer from "puppeteer-core";

    const TOKEN = "YOUR_API_TOKEN_HERE";

    const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
    });

    try {
    const page = await browser.newPage();
    await page.goto("https://example.com", { waitUntil: "networkidle2" });

    const title = await page.title();
    const heading = await page.$eval("h1", (el) => el.textContent);

    console.log({ title, heading });
    } finally {
    // Release the session even if something above threw.
    await browser.close();
    }

    Output

    { title: 'Example Domain', heading: 'Example Domain' }

Configuring the browser

Options you used to pass to launch() now travel on the connection URL, because the browser starts before your code ever talks to it.

const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}&blockAds=true&timeout=60000`,
});

Array-valued options such as Chrome args go in the JSON launch parameter instead, since their brackets and commas need encoding. The launch parameters reference has the full list and both syntaxes.

What changes and what doesn't

Page-level code is unchanged: page.goto, page.$eval, page.waitForSelector, page.pdf, and the rest behave as they do locally. Three things are worth knowing:

  • browser.close() ends a remote session, not a local process. Skip it and the session stays alive until it times out, and you're billed for that time. Put it in a finally block.
  • Local file paths don't exist on the browser's machine. Downloads and uploads go through the file transfer APIs rather than your own disk.
  • Latency is between the browser and the target site, not between you and the browser. Pick the region closest to the sites you're scraping. See connection URLs.

The BAP alternative

BAP is our own SDK, and its TypeScript API borrows Puppeteer's method names, so the script above is close to what you'd write with it:

import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

// connect() opens no socket. The WebSocket opens on newPage().
const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
token: TOKEN,
});

try {
const page = await browser.newPage();
await page.goto("https://example.com");

const title = await page.title();
const { text } = await page.text({ selector: "h1" });

console.log({ title, heading: text });
} finally {
await browser.close();
}

Output

{ title: 'Example Domain', heading: 'Example Domain' }

It isn't a drop-in replacement. BAP runs over BrowserQL rather than a live CDP session, so page.mouse, page.keyboard, frames, and browser contexts have no equivalent, and shared names like $eval behave differently: it returns the matched element's text rather than running your callback. What you get in exchange is solve(), proxy(), markdown(), and liveURL() as methods instead of plumbing.

Keep this page's connect() approach when your script needs direct browser control. See Migrate to BAP for the full method-by-method comparison.

FAQ & Troubleshooting

Why does puppeteer.connect() hang or time out?

Usually the URL. It must start with wss:// (not https://) and carry ?token=. A missing or invalid token closes the socket during the handshake, which surfaces as a hang rather than a clean 401.

Can I still use puppeteer instead of puppeteer-core?

Yes, connect() works from either package. You'll just be downloading and storing a Chromium build you never launch, which slows installs and bloats CI images.

My script works locally but the page renders differently on Browserless.

The remote browser has its own viewport, user agent, timezone, and locale. Set them explicitly with launch parameters so both environments match, rather than relying on local defaults.

Do concurrent scripts need separate connections?

Yes. Each puppeteer.connect() call takes its own session and counts against your plan's concurrency limit. Reuse one browser object across pages within a script, and open a new connection per parallel job.

Next steps

Was this page helpful?