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:
- 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.
- 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.
- 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-coreversion, 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.