· Updated · Comparisons · By Simon Chiu

Best Headless Browser APIs for Screenshots (2026)

Browserless, Browserbase, Cloudflare Browser Rendering, Steel and ScrapingBee compared for capture work, with connection code and a benchmark harness.

Line illustration of the RenderScreenshot mascot comparing three server racks that each run a browser

The best headless browser API depends on what you do after the page loads. Browserbase and Steel are built for long, stateful sessions such as AI agents and logged-in flows. Browserless is the most direct swap for a Puppeteer or Playwright fleet you run today. Cloudflare Browser Rendering is the cheapest path if your code already runs on Workers. ScrapingBee is not a browser you drive at all; it is an HTTP API that returns HTML or a screenshot. If all you need is the image, a screenshot API is less code than any of them, and that is covered in our screenshot API comparison.

Disclosure: I build RenderScreenshot. We are a screenshot API, not a headless browser API, and our renders run on Cloudflare Browser Rendering. That makes us a Cloudflare customer, not a neutral party, so read the Cloudflare section with that in mind.

Method and status of this page: this first version compares the five services on how you connect, what you pay for, and what each is good at, based on each vendor's documented connection and billing model. Vendor features and prices change often, so confirm the details on each vendor's site before you buy. It also includes the benchmark harness we will use for the measured run, and a baseline from a local Chromium on the same tasks. Hosted-vendor timings are not in this version: we publish numbers only after we have run each service with its own account on the same task list. The harness is below, so you can run it today.

Headless browser API, screenshot API, scraping API

The three names get used for each other. They are different products:

Headless browser API Screenshot API Scraping API
What you send Puppeteer or Playwright commands over a WebSocket One HTTP request: URL plus options One HTTP request: URL plus options
What you get back A live browser session An image or PDF HTML, JSON or a screenshot
Who writes the waits, clicks and retries You The provider The provider
Billing unit (typical) Browser time Per capture Per request, weighted by features
Good for Logins, multi-step flows, agents, custom scraping Social cards, previews, archives, monitoring Data extraction behind anti-bot systems
Examples Browserless, Browserbase, Steel, Cloudflare Browser Rendering RenderScreenshot, ScreenshotOne, Urlbox ScrapingBee, ScraperAPI

If you are not sure which group you need, the screenshot API vs headless browser API comparison walks through the decision with code for both.

The five services at a glance

Service How you connect Libraries Self-host option Billing model (per vendor docs; confirm current terms) Best for
Browserless WebSocket URL with a token; also REST endpoints Puppeteer, Playwright, its own query language Docker image (check licence for commercial use) Units of browser time, by plan Moving an existing Puppeteer/Playwright fleet off your servers
Browserbase Create a session with the SDK or API, then connect over CDP Playwright, Puppeteer, Selenium, Stagehand No Browser hours, by plan Agents and long sessions with recordings and saved logins
Cloudflare Browser Rendering Workers binding, or a REST API Cloudflare forks of Puppeteer and Playwright No Browser time plus concurrent browsers, on Workers plans Code that already runs on Cloudflare Workers
Steel Create a session with the SDK, then connect over CDP Playwright, Puppeteer, Selenium Yes, open-source server Session time, by plan Teams that want a hosted service and a self-hostable fallback
ScrapingBee Plain HTTP GET Any HTTP client No Credits per request, more for JS rendering and premium proxies Pages behind anti-bot systems, data extraction

We list no prices here on purpose. All five change plans often, and most bill on time or weighted credits, so a list price tells you little until you know how long your sessions run. Use the cost model further down with each vendor's current pricing page.

What we measure, and how

The harness runs three tasks against any browser you can reach with chromium.connectOverCDP(): a 1280x800 viewport screenshot, a full-page screenshot and an A4 PDF. For each task it opens a fresh connection, so the numbers include the connection set-up that you pay for on every job.

// bench-cdp.mjs: time the same capture tasks on any browser you can reach over CDP.
// Usage: CDP_URL=wss://... node bench-cdp.mjs 5 > timings.csv
import { chromium } from 'playwright';

const RUNS = Number(process.argv[2] ?? 3);
const TARGET = process.env.TARGET_URL ?? 'https://example.com';
const tasks = {
  viewport: (page) => page.screenshot({ type: 'png' }),
  full_page: (page) => page.screenshot({ type: 'png', fullPage: true }),
  pdf: (page) => page.pdf({ format: 'A4' }),
};

const ms = (t0) => Math.round(performance.now() - t0);
console.log('run,task,connect_ms,goto_ms,capture_ms,total_ms,bytes');

for (let run = 1; run <= RUNS; run++) {
  for (const [name, task] of Object.entries(tasks)) {
    const t0 = performance.now();
    const browser = await chromium.connectOverCDP(process.env.CDP_URL, { timeout: 30_000 });
    const connectMs = ms(t0);
    try {
      // Hosted providers usually hand you a default context; reuse it if present.
      const context = browser.contexts()[0] ?? (await browser.newContext());
      const page = await context.newPage();
      await page.setViewportSize({ width: 1280, height: 800 });
      const t1 = performance.now();
      await page.goto(TARGET, { waitUntil: 'load', timeout: 30_000 });
      const gotoMs = ms(t1);
      const t2 = performance.now();
      const buf = await task(page);
      console.log([run, name, connectMs, gotoMs, ms(t2), ms(t0), buf.length].join(','));
      await page.close();
    } finally {
      await browser.close();
    }
  }
}

Tested on 2026-10-15 with Node.js 22.18.0 and Playwright 1.64.1, against a local Chrome Headless Shell 156 started with --remote-debugging-port=9333 (CDP_URL=http://127.0.0.1:9333). For hosted services, set CDP_URL to the WebSocket URL each one gives you (connection code per vendor below). Services that create a session per connection need the session-creation call inside the loop; the Browserbase and Steel snippets show where it goes.

Local baseline

Three runs on an Apple M1 Pro, target https://example.com. This is the floor: no network hop to the browser and no session start-up on a remote host.

Task Connect (ms) Navigate (ms) Capture (ms) Total (ms) Bytes
Viewport PNG 231 / 16 / 9 269 / 69 / 64 70 / 153 / 87 616 / 308 / 214 80,922
Full-page PNG 8 / 61 / 14 81 / 72 / 75 99 / 82 / 78 234 / 261 / 236 80,922
A4 PDF 19 / 18 / 12 70 / 63 / 69 35 / 35 / 37 180 / 167 / 170 189,124

The first connection is slower (231 ms) because the browser warms up. After that, connecting to an already-running browser costs tens of milliseconds. On a hosted service, the connect step also includes starting or assigning a browser on their side, which is where the services differ most. That is the number to compare first when the run is published.

Browserless

What it is: hosted Chrome that you reach with a WebSocket URL. You keep your Puppeteer or Playwright code and change launch() to connect(). It also has REST endpoints for screenshots, PDFs and page content, and its own query language for scripted automation.

// Untested here: needs a Browserless token.
import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_TOKEN}`,
);
try {
  const page = await browser.contexts()[0].newPage();
  await page.goto('https://example.com', { waitUntil: 'load' });
  await page.screenshot({ path: 'browserless.png' });
} finally {
  await browser.close();
}

The region in the host name (production-sfo above) comes from your account dashboard; use the one it shows you.

Strengths: the smallest code change from a self-hosted fleet. Launch flags and query parameters on the WebSocket URL control things like stealth and ad blocking. The self-hostable Docker image means you can develop locally against the same server you use in production.

Weaknesses: you still own all the capture logic: waits, retries, cookie banners, lazy loading and storage. Read the licence terms before you run the Docker image in a commercial product.

Best for: teams with working Puppeteer or Playwright code who want to stop running Chrome on their own servers. Our Browserless alternative page covers the case where most of that code only takes screenshots.

Browserbase

What it is: a session platform. You create a session through the SDK or API, then connect to it over CDP. Sessions can be recorded and replayed, watched live, and started with a saved context (cookies and storage) so an agent stays logged in. Browserbase also maintains Stagehand, a framework that mixes Playwright with natural-language actions.

// Untested here: needs a Browserbase API key and project ID.
import Browserbase from '@browserbasehq/sdk';
import { chromium } from 'playwright';

const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
const session = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID });

const browser = await chromium.connectOverCDP(session.connectUrl);
try {
  const page = browser.contexts()[0].pages()[0];
  await page.goto('https://example.com', { waitUntil: 'load' });
  await page.screenshot({ path: 'browserbase.png' });
} finally {
  await browser.close();
}

Strengths: the strongest debugging story of the group (session recordings, live view), first-class support for persistent logins, and tooling aimed at AI agents.

Weaknesses: a session per job adds a creation step before each connect, and billing is on session time, so a fleet of short screenshot jobs pays for the set-up time of every session. Our Browserbase alternative page has the cost-per-screenshot arithmetic for capture-only workloads.

Best for: agents, multi-step flows and anything you need to debug by watching a replay.

Cloudflare Browser Rendering

What it is: headless Chromium inside Cloudflare's network, driven from a Worker through a binding, or called through a REST API for one-shot screenshots, PDFs, HTML, Markdown and links. Cloudflare ships forks of Puppeteer (@cloudflare/puppeteer) and Playwright (@cloudflare/playwright) that talk to the binding.

// Untested here: runs inside a Cloudflare Worker with a [browser] binding named MYBROWSER.
import puppeteer from '@cloudflare/puppeteer';

export default {
  async fetch(request, env) {
    const browser = await puppeteer.launch(env.MYBROWSER);
    try {
      const page = await browser.newPage();
      await page.setViewport({ width: 1280, height: 800 });
      await page.goto('https://example.com', { waitUntil: 'load' });
      const png = await page.screenshot({ type: 'png' });
      return new Response(png, { headers: { 'content-type': 'image/png' } });
    } finally {
      await browser.close();
    }
  },
};

Strengths: no servers, no WebSocket URLs to manage, and the rendered bytes can go straight to R2 or the cache in the same Worker. Pricing is based on browser time and concurrency on top of a Workers plan, which suits short jobs. This is the platform RenderScreenshot runs on.

Weaknesses: you work inside the Workers runtime (CPU and duration limits apply to your Worker code), the library forks trail upstream Puppeteer and Playwright releases, and there are account-level limits on concurrent browsers and on how fast you can launch new ones. We wrote up the practical details, including session reuse and the launch-rate limit, in our Cloudflare Browser Rendering tutorial and the Playwright on Browser Rendering guide.

Best for: teams already on Workers, and short, high-volume jobs where per-second browser billing is cheaper than reserved capacity.

Steel

What it is: a browser API for agents with hosted sessions and an open-source server you can run yourself. As with Browserbase, you create a session and then connect with Playwright, Puppeteer or Selenium.

// Untested here: needs a Steel API key.
import Steel from 'steel-sdk';
import { chromium } from 'playwright';

const steel = new Steel({ steelAPIKey: process.env.STEEL_API_KEY });
const session = await steel.sessions.create();

const browser = await chromium.connectOverCDP(
  `wss://connect.steel.dev?apiKey=${process.env.STEEL_API_KEY}&sessionId=${session.id}`,
);
try {
  const page = browser.contexts()[0].pages()[0] ?? (await browser.contexts()[0].newPage());
  await page.goto('https://example.com', { waitUntil: 'load' });
  await page.screenshot({ path: 'steel.png' });
} finally {
  await browser.close();
  await steel.sessions.release(session.id);
}

Strengths: the open-source server lets you start self-hosted and move to the hosted service (or back) without rewriting code. Session features such as proxies and a session viewer target agent workloads.

Weaknesses: a younger ecosystem than Browserless or Browserbase, so expect fewer integrations and examples. Release sessions explicitly, as above, so they do not run until they time out.

Best for: agent builders who want an exit path to self-hosting.

ScrapingBee

What it is: an HTTP scraping API. You send a URL and options in a GET request, and it returns HTML, extracted data or a screenshot. JavaScript rendering, proxies and scripted actions (js_scenario) are request options, not code you run in a browser.

# Untested here: needs a ScrapingBee API key.
curl -G "https://app.scrapingbee.com/api/v1/" \
  --data-urlencode "api_key=$SCRAPINGBEE_API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "screenshot=true" \
  -o scrapingbee.png

Strengths: the best fit in this list for pages that block data-centre traffic, because proxy rotation and anti-bot handling are built in. No browser code at all.

Weaknesses: credits per request depend on the options you turn on, so the same screenshot can cost several times more with premium proxies. Screenshot options are fewer than in a dedicated screenshot API. It is the wrong tool for multi-step sessions.

Best for: extraction from hard-to-reach pages, with a screenshot as a side product.

Best pick by job

Job Pick Runner-up
Move an existing Puppeteer/Playwright fleet off your servers Browserless Steel
AI agent that logs in and clicks through flows Browserbase Steel
Short, high-volume renders from code already on Cloudflare Cloudflare Browser Rendering Browserless
Pages that block data-centre IPs ScrapingBee Browserbase with proxies
Self-host now, hosted later Steel (open-source server) Browserless (Docker image, check licence)
Only screenshots, PDFs or social cards A screenshot API Cloudflare Browser Rendering REST endpoints

A cost model you can fill in

Session-billed services charge for the time a browser is open, not for the screenshot. Measure your own average session length with the harness (the total_ms column), then:

browser minutes per month = captures per month x average session seconds / 60
cost per capture          = (plan price + overage for browser minutes) / captures per month

Two things move this number more than the list price:

  1. Session set-up time. A hosted session that takes a few seconds to start, for a capture that takes half a second, means most of your bill is set-up. Reusing one session for many captures (where the service allows it) cuts this.
  2. Minimum billing increments. Some services round each session up to a minimum. Check the billing page for the increment, then compare it with your average session length.

For request-billed services (ScrapingBee and screenshot APIs), the formula is plan price divided by the requests you use, adjusted by the credit multiplier for the options you need. For RenderScreenshot, one standard capture is one credit, and cached repeats can be served without a new render. Our plans are on the pricing page.

Ten questions to ask any headless browser vendor

Pricing pages answer the easy questions. These are the ones that decide whether a hosted browser works for your workload, and most of them are answered only in the docs, the limits page or a support ticket:

Question Why it matters for capture work
How long does a new session take to become usable, at p50 and p95? For short jobs, set-up time can be most of the billed time
What is the concurrency cap per plan, and what happens above it: queue or HTTP 429? A queue adds latency; a 429 needs retry code
Is there a separate limit on how fast you can start sessions? Bursty jobs hit launch-rate limits before concurrency limits
What is the minimum billing increment per session? Rounding a 2-second job up to a minute multiplies cost
How long does an idle session live before it is closed and billed? A crashed worker that never calls close() keeps the meter running
Which Chromium version runs, and how fast does it follow upstream? Your local Playwright or Puppeteer version must speak the same protocol
Can you pin a region, and where is it? A browser far from the target site adds a round trip to every request
Are recordings, logs and downloaded files stored, and for how long? Captures of logged-in pages may contain personal data
Which fonts and locales are installed? Missing CJK or emoji fonts show up as empty boxes in screenshots
What does the service do about bot detection, and is it on by default? Stealth and proxies change what the page shows you, and often cost extra

Run the harness above to answer the first question with numbers for your region; ask the vendor for the rest in writing before you commit to an annual plan.

Where the time goes in a remote capture

On a local browser, the baseline table shows that navigation and capture dominate once the browser is warm. On a hosted browser, three more costs appear, and it helps to know which one you are looking at when a run is slow:

  1. Session start. The provider assigns or boots a browser. This is the connect_ms column, plus the session-creation call for services that need one. It is the number that varies most between providers and between cold and warm pools.
  2. Browser-to-site distance. Every request the page makes goes from the provider's data centre to the target site. A site hosted in Frankfurt captured from a US region pays a transatlantic round trip on each of dozens of requests. This shows up in goto_ms.
  3. Result transfer. The PNG or PDF travels back over the WebSocket to your code. A full-page capture of a long page can be several megabytes, which shows up in capture_ms on slow links.

Measure from the region where your jobs run, against the sites you capture, and compare the three columns separately rather than only the total.

When a headless browser API is the wrong choice

If your jobs are "open URL, wait, capture, store", a headless browser API makes you write and maintain the parts a screenshot API already does: wait strategies, cookie-banner handling, lazy-image scrolling, retries, storage and caching. That code is where most screenshot bugs live. In that case, compare the two approaches with the same URLs before you commit; the vendor test script works for screenshot APIs, and bench-cdp.mjs above works for browser APIs.

If your jobs need a browser to stay open across steps, a screenshot API is the wrong choice and one of the five services above is the right one.

Run the harness yourself

  1. Install the harness dependencies: npm i [email protected].
  2. Get a CDP URL from each service you are considering (connection code above).
  3. Run CDP_URL=... node bench-cdp.mjs 5 > vendor.csv from the region where your jobs will run.
  4. Compare connect_ms and total_ms at the median and the 95th percentile, not the average.

If you also want to see what the same captures cost as plain API calls, create a free RenderScreenshot key and run the screenshot API test next to it.

Changelog

  • 2026-10-15: Latest revision: service overview from vendor documentation, connection code, harness, local baseline. Hosted-vendor timings to follow after a measured run on each service.

Related in this topic