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.
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:
- 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.
- 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:
- Session start. The provider assigns or boots a browser. This is the
connect_mscolumn, 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. - 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. - 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_mson 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
- Install the harness dependencies:
npm i [email protected]. - Get a CDP URL from each service you are considering (connection code above).
- Run
CDP_URL=... node bench-cdp.mjs 5 > vendor.csvfrom the region where your jobs will run. - Compare
connect_msandtotal_msat 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 guides
Related in this topic
- Best Screenshot API Comparison (2026): 10 Services The best screenshot APIs of 2026 compared: 10 services plus self-hosted Chrome, with a scored checklist, prices checked in October 2026 and a test script.
- Screenshot API Alternatives Alternatives to screenshot APIs, OG image tools, PDF APIs and headless browser libraries, with honest notes on when each option is the better fit.
- Screenshot API Pricing Comparison: 10 Vendors (2026) Screenshot API pricing compared at 1,000, 10,000 and 100,000 shots a month for 10 vendors, with cost per 1,000, billing-unit traps and a calculator.
- ApiFlash Alternative: Parameter Map and Migration An ApiFlash alternative with signed links and presets: full parameter map, ApiFlash pricing at three volumes, and tested Node, Python and Ruby migration code.
- Browserless Alternative for Screenshots A Browserless alternative for screenshots: browser infrastructure vs a screenshot API, cost per screenshot, and tested /screenshot migration code.
- Microlink Alternative for Screenshots: Tested Switch A Microlink alternative for screenshots: live Microlink timings, its 2x pixel-ratio default, a parameter map, pricing and migration code in 3 languages.