Browserbase vs Browserless for Screenshot Workloads

Browserbase vs Browserless for screenshots: session model, billing units, cost per capture at 1k to 100k a month, connection code and a timing harness.

By Simon Chiu · Updated

Disclosure: I build RenderScreenshot, a screenshot API. We are not a reseller or partner of either vendor. I have not run this benchmark against either live service yet: the timing harness below is tested against a local browser only, and every product fact is taken from the vendors' own docs and pricing pages as read on 2026-10-15.

Short answer: both give you Chrome in the cloud that you drive with Puppeteer or Playwright, and both will take screenshots well. The difference is the shape of the product. Browserless is a browser endpoint first: one WebSocket URL with a token, REST routes for common jobs like /screenshot and /pdf, and a self-hosting path. Browserbase is a session platform first: you create a session through an API, then connect, and you get session replays, a live debug view, persisted contexts and agent tooling. For screenshot workloads, the billing models matter more than the features: Browserless charges per connection in 30-second blocks, Browserbase charges per browser hour.

If you are still choosing between running a browser at all and calling a screenshot API, read screenshot API vs headless browser API first. More vendor pairs are in the comparison hub.

Side by side

Browserless Browserbase
How you connect puppeteer.connect({ browserWSEndpoint: 'wss://production-sfo.browserless.io?token=...' }) bb.sessions.create() then puppeteer.connect({ browserWSEndpoint: session.connectUrl })
One-call REST screenshot Yes, POST /screenshot?token=... with a JSON body No dedicated screenshot route; you capture inside a session
Frameworks Puppeteer, Playwright, its own BrowserQL Puppeteer, Playwright, Selenium, its Stagehand agent framework
Browsers listed Chrome, WebKit and Firefox Chrome-based in the quickstarts we read
Billing unit Unit = up to 30 s of browser time per connection; a reconnect starts a new unit Browser hours
Concurrency (paid entry plan) 10 (+5) on Prototyping 25 on Developer
Max session length (entry plans) 2 min Free, 15 min Prototyping 15 min per session on Free
Debugging Session replays and a log search window, retention by plan Live view, session recording and an inspector
Persisting logins Persisted sessions (days, by plan) Contexts (reusable cookies and storage)
Regions listed on the pricing page San Francisco, London, Amsterdam Not listed there
Self-hosting Docker, plus licensed self-hosting Managed only

Sources: Browserless pricing, Browserless docs, Browserbase pricing, Browserbase Puppeteer quickstart. Plans and limits change; re-check before you sign up.

Code for both

Both examples use puppeteer-core, because the browser runs remotely and you do not need a local Chrome download. Tokens come from environment variables.

Browserless: connect with a token

import puppeteer from 'puppeteer-core';
import { writeFile } from 'node:fs/promises';

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_TOKEN}`,
});
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });
  await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 30_000 });
  await writeFile('browserless.png', await page.screenshot({ type: 'png' }));
} finally {
  await browser.close(); // ends the connection, and the unit count stops here
}

Or, with no Puppeteer at all, the REST route. Browserless's docs note that the shared cloud expects the token in the query string on these routes, so keep the URL out of shell history and logs:

printf 'url = "https://production-sfo.browserless.io/screenshot?token=%s"\n' "$BROWSERLESS_TOKEN" \
  | curl -sS -X POST -K- \
      -H "Content-Type: application/json" \
      -d '{"url": "https://example.com"}' \
      --fail --output browserless-rest.png

Browserbase: create a session, then connect

import puppeteer from 'puppeteer-core';
import Browserbase from '@browserbasehq/sdk';
import { writeFile } from 'node:fs/promises';

const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
// A short timeout caps billed time if the script dies before cleanup.
const session = await bb.sessions.create({ timeout: 60, keepAlive: false });

const browser = await puppeteer.connect({ browserWSEndpoint: session.connectUrl });
try {
  const [page] = await browser.pages();
  await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 30_000 });
  await writeFile('browserbase.png', await page.screenshot({ type: 'png' }));
  console.log(`Replay: https://browserbase.com/sessions/${session.id}`);
} finally {
  await browser.close();
}

Browserbase's screenshot docs recommend calling the DevTools Page.captureScreenshot command through a CDP session for speed. That is worth testing on large full-page captures; for a viewport PNG, page.screenshot() is the simpler call.

What it costs per screenshot

Billing is where these two differ for screenshot jobs. A typical capture keeps a browser busy for a few seconds. In our local runs, launch to close for one viewport capture of example.com took 1.5 to 1.9 seconds; on a hosted browser, network round trips add more. For the math below I assume 3 seconds per capture and one connection per capture. That is an assumption, not a measurement.

Monthly captures Browserless (units) Browserless plan that covers it Browserbase (browser hours) Browserbase plan that covers it
1,000 1,000 Free (1k units) 0.8 h Free covers 1 h, with 3 concurrent browsers
10,000 10,000 Prototyping, $25/mo billed annually (20k units) 8.3 h Developer, $20/mo (100 h)
100,000 100,000 Starter, $140/mo billed annually (180k units) 83 h Developer, $20/mo (100 h), if concurrency of 25 is enough

Prices as listed on the vendors' pricing pages on 2026-10-15. Browserless shows annual-billing prices by default; monthly billing costs more.

Three things change this table in practice:

  1. Batching. Browserless counts per connection in 30-second blocks. Capture five URLs in one connection inside 30 seconds and you pay one unit, not five. At 100k captures, batching can move you down a plan.
  2. Rounding and minimums on Browserbase. The hour math above assumes billing close to actual seconds. Read how Browserbase rounds session time before you plan around it. A per-session minimum would change the result for short captures.
  3. Concurrency. Spiky traffic (for example, a social post that makes thousands of link previews at once) needs headroom. Browserless lists 10 (+5) concurrent browsers on Prototyping and 40 (+10) on Starter. Browserbase lists 25 on Developer and 100 on Startup.

Head-to-head timing: how to run it

We publish measured numbers on other comparison pages. For this pair we have not yet run the live benchmark, so instead of numbers, here is the method and a harness you can run with your own keys. It is the same harness we will use when we add results to this page.

Method:

  • Same script, same puppeteer-core version, run from one machine, one provider at a time.
  • One fresh session per capture (the worst case for billing, and the common case for on-demand screenshots).
  • Record session setup time, navigation time, screenshot time and total, plus output bytes and a PNG signature check.
  • At least 5 runs per URL; report median and p95 separately for each region you test.
  • Use pages that stress the browser differently: a static page, a JavaScript-heavy app, a long page with lazy images.

Harness (tested on 2026-10-15 with Node 22.18.0 and [email protected] against a local browser; the remote providers need your keys):

// Time one screenshot per fresh session on a hosted browser.
// Usage: node bench.mjs <browserless|browserbase|local> [runs] [url...]
// Env: BROWSERLESS_TOKEN, BROWSERLESS_HOST (default production-sfo.browserless.io), BROWSERBASE_API_KEY
import puppeteer from 'puppeteer-core';
import { appendFile } from 'node:fs/promises';

const [provider = 'local', runsArg = '5', ...urls] = process.argv.slice(2);
const RUNS = Number(runsArg);
const targets = urls.length ? urls : ['https://example.com'];

// Each provider returns { browser, sessionMs }.
const providers = {
  async browserless() {
    const host = process.env.BROWSERLESS_HOST ?? 'production-sfo.browserless.io';
    const t0 = performance.now();
    const browser = await puppeteer.connect({
      browserWSEndpoint: `wss://${host}?token=${process.env.BROWSERLESS_TOKEN}`,
    });
    return { browser, sessionMs: performance.now() - t0 };
  },
  async browserbase() {
    const { default: Browserbase } = await import('@browserbasehq/sdk');
    const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
    const t0 = performance.now();
    const session = await bb.sessions.create({ timeout: 60, keepAlive: false });
    const browser = await puppeteer.connect({ browserWSEndpoint: session.connectUrl });
    return { browser, sessionMs: performance.now() - t0 };
  },
  async local() {
    const { default: full } = await import('puppeteer'); // bundled Chrome, for checking the harness
    const t0 = performance.now();
    const browser = await full.launch();
    return { browser, sessionMs: performance.now() - t0 };
  },
};
if (!providers[provider]) throw new Error(`unknown provider ${provider}`);

const out = `bench-${provider}.csv`;
await appendFile(out, 'provider,url,run,session_ms,goto_ms,shot_ms,total_ms,bytes,ok\n');
for (const url of targets) {
  for (let run = 1; run <= RUNS; run++) {
    const t0 = performance.now();
    let row = { session: 0, goto: 0, shot: 0, bytes: 0, ok: false };
    let browser;
    try {
      const s = await providers[provider]();
      browser = s.browser;
      row.session = s.sessionMs;
      const page = await browser.newPage();
      await page.setViewport({ width: 1280, height: 800 });
      const t1 = performance.now();
      await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
      const t2 = performance.now();
      const png = await page.screenshot({ type: 'png' });
      row = { ...row, goto: t2 - t1, shot: performance.now() - t2, bytes: png.length, ok: png.subarray(1, 4).toString() === 'PNG' };
    } catch (err) {
      console.error(`${provider} ${url} run ${run}: ${err.message}`);
      process.exitCode = 1;
    } finally {
      await browser?.close().catch(() => {}); // ends the billed session
    }
    const total = performance.now() - t0;
    const line = [provider, url, run, row.session, row.goto, row.shot, total].map((v) => (typeof v === 'number' ? v.toFixed(0) : v));
    await appendFile(out, [...line, row.bytes, row.ok].join(',') + '\n');
    console.log(line.join('\t'), row.bytes, row.ok);
  }
}

Install with npm i [email protected] @browserbasehq/sdk (add [email protected] only for the local check). Run node bench.mjs browserless 5 https://example.com https://your-app.example/dashboard, then the same with browserbase. Against a local browser, the harness recorded 0.4 to 0.7 seconds of launch time and about 1 second of navigation for example.com with networkidle2, which gives you a floor to compare the hosted numbers against.

When you read your results, separate session setup from navigation. Setup is the vendor's cost; navigation is mostly the target site and the distance between the vendor's region and that site.

When to choose which

Choose Browserless if:

  • You want a single connect URL that drops into existing Puppeteer or Playwright code.
  • You want REST routes for screenshots and PDFs without writing browser code.
  • You may need to self-host later, or run in your own infrastructure for data rules.
  • You can batch several captures per connection to make the 30-second unit go further.

Choose Browserbase if:

  • Your workload is agent-style browsing or multi-step flows where session replays and a live view save debugging time.
  • You need persisted contexts to reuse logins across many sessions.
  • Your captures are short and you want billing that follows browser time closely.
  • You use Selenium or Stagehand and want first-party quickstarts for them.

Choose neither if your job is "URL in, image out". Both make you write and operate the capture code: waits, lazy loading, blocking, retries, storage. A screenshot API does that per request. With RenderScreenshot the whole Browserless or Browserbase script above is one call:

import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);
await writeFile('example.png', await client.take(TakeOptions.url('https://example.com').width(1280).height(800)));

Responses are cached by default and carry an X-Cache-URL CDN link (see caching), which a raw browser session does not give you. To compare all three on your own URLs, get a free API key and add a third provider to the harness above.

Last verified: 2026-10-15 (docs and pricing). Live benchmark pending; this page will show the run date when results are added.

Frequently asked questions

Which is cheaper for screenshots, Browserbase or Browserless?
For short, one-off captures, Browserbase's per-hour billing works out lower per capture, provided its rounding rules do not add a large minimum per session. Browserless bills a unit for each connection of up to 30 seconds, so one capture per connection costs one unit.
Does Browserless have a screenshot endpoint?
Yes. Besides the WebSocket connect endpoint, Browserless documents REST routes including /screenshot, /pdf and /content that take a URL in a JSON body.
Can I self-host either one?
Browserless documents self-hosting with Docker and offers licensed self-hosting for enterprises. Browserbase is a managed service.
Do my Puppeteer scripts work on both?
Yes. Both accept puppeteer.connect with a WebSocket endpoint. Browserless gives you a fixed URL with a token; Browserbase creates a session through its API first and returns a connectUrl.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.