If you use Browserbase to open a URL, wait and take a screenshot, you are paying for a session platform to do the job of a screenshot API. RenderScreenshot replaces that whole session (create, connect, navigate, wait, capture, close) with one HTTP request, and bills per capture instead of per browser-minute. If your sessions log in through forms, click through flows or run AI agents, Browserbase is the better tool, and this page says so.
Disclosure: I build RenderScreenshot. Browserbase details on this page describe its documented session model (create a session, connect over CDP, pay for browser time). Check Browserbase's documentation and pricing for current plans and limits before you decide.
This page sits in our screenshot API comparison series. For a wider view of hosted browsers, including Browserless, Steel and Cloudflare Browser Rendering, see the best headless browser APIs.
Verdict
| Choose Browserbase if | Choose RenderScreenshot if |
|---|---|
| Your job logs in through a UI, clicks, types or follows a multi-step flow | Your job is "open URL, wait, capture, store" |
| You run AI agents that decide what to do next on the page | You want images, PDFs or social cards with presets |
| You need session recordings and a live view to debug | You want a typed error and a retry flag instead of a recording |
| You need to keep logged-in browser contexts between runs | You can pass the session as cookies or headers |
| Your captures are a side effect of a larger automation | Captures are the product (previews, archives, OG images) |
Session platform vs capture API
Browserbase sells browser time. You create a session, connect Playwright, Puppeteer or Selenium to it over the Chrome DevTools Protocol, and pay while it is open. Everything that happens inside the session is your code: viewport, waits, cookie banners, lazy images, retries, where the file goes.
RenderScreenshot sells captures. You send the URL and options; the service runs the browser, applies the waits and blocking you asked for, and returns the image. The browser lifecycle, the retries inside the render and the cache are ours to run.
The practical difference for a screenshot job:
| Step | Browserbase (your code) | RenderScreenshot (one request) |
|---|---|---|
| Start a browser | bb.sessions.create() then chromium.connectOverCDP(session.connectUrl) |
Not needed |
| Set the viewport | page.setViewportSize({ width: 1280, height: 800 }) |
viewport.width, viewport.height (or width, height in GET) |
| High-DPI output | deviceScaleFactor on a new context |
viewport.scale (1 to 3) |
| Mobile device | Playwright device descriptor | viewport.device, for example iphone_14_pro |
| Wait for the page | page.goto(url, { waitUntil: 'networkidle' }) |
wait.until: "networkidle" |
| Wait for an element | page.locator(sel).waitFor() |
wait.for_selector |
| Extra settle time | A fixed sleep after load | wait.delay (ms) |
| Hide or remove elements | page.addStyleTag(...) |
page.hide, page.remove |
| Cookie banners and ads | Your own selectors or extensions | block.cookie_banners, block.ads, block.chat_widgets |
| Logged-in pages | Saved context or a scripted login | network.cookies, network.headers, network.auth |
| Full page | page.screenshot({ fullPage: true }) |
capture.mode: "full_page" with capture.scroll for lazy images |
| One element | locator.screenshot() |
capture.mode: "element" with capture.selector |
page.pdf({ format: 'A4' }) |
output.format: "pdf" with the pdf options |
|
| Store the file | Your code uploads it | X-Cache-URL CDN link, or storage to your own bucket |
| Close the browser | browser.close() (or you keep paying) |
Not needed |
Every RenderScreenshot name in that table is documented in the parameter reference, wait options and capture options.
Cost per screenshot, worked out
Browserbase bills browser hours by plan (see their pricing page for the rates and included hours). RenderScreenshot bills one credit per standard capture. The two only become comparable once you know how many billed seconds one capture takes in your Browserbase account: session creation, connect, navigation, waits and capture, plus any minimum billing increment per session.
Measure that number first. The bench-cdp.mjs harness from our headless browser API comparison records it per run; for Browserbase, put the sessions.create() call inside its loop. Then compute the break-even browser-hour price:
// break-even.mjs: at what browser-hour price does a session platform cost the same as a per-capture API? // Usage: node break-even.mjs <captures per month> <average billed seconds per capture> const [captures, seconds] = process.argv.slice(2).map(Number); if (!captures || !seconds) { console.error('usage: node break-even.mjs 10000 8'); process.exit(2); } // RenderScreenshot self-serve plans, checked 2026-10-15. One credit per standard capture. const plans = [ { name: 'Starter', price: 10, credits: 2_500 }, { name: 'Growth', price: 40, credits: 12_000 }, { name: 'Scale', price: 100, credits: 60_000 }, ]; const plan = plans.find((p) => p.credits >= captures); if (!plan) { console.log('Above 60,000 captures a month: compare against a custom plan quote.'); process.exit(0); } const perCapture = plan.price / captures; const browserHours = (captures * seconds) / 3600; const breakEven = plan.price / browserHours; console.log(`RenderScreenshot ${plan.name}: $${plan.price}/month = $${perCapture.toFixed(4)} per capture`); console.log(`Session platform: ${browserHours.toFixed(1)} browser-hours/month at ${seconds}s per capture`); console.log(`Break-even: $${breakEven.toFixed(2)} per browser-hour, before base fees and included hours`);
Tested on 2026-10-15 with Node.js 22.18.0. The output for the volumes and session lengths most screenshot jobs fall into:
| Captures per month | RenderScreenshot plan | Per capture | Break-even at 5 s | at 10 s | at 20 s |
|---|---|---|---|---|---|
| 1,000 | Starter, $10 (2,500 credits) | $0.0100 | $7.20 / browser-hour | $3.60 | $1.80 |
| 10,000 | Growth, $40 (12,000 credits) | $0.0040 | $2.88 / browser-hour | $1.44 | $0.72 |
| 60,000 | Scale, $100 (60,000 credits) | $0.0017 | $1.20 / browser-hour | $0.60 | $0.30 |
| 100,000 | Custom plan (above Scale) | Quote | Quote | Quote | Quote |
How to read it: if your effective Browserbase cost per browser-hour, after base fees and included hours, is below the break-even number for your row, Browserbase is cheaper for that workload on raw compute. If it is above, the API is cheaper. Two effects move you across the line:
- Long waits make sessions expensive. A single-page app that needs 15 seconds of network activity before it settles costs three times as much on a time-billed session as one that settles in 5 seconds. On a per-capture API, the wait does not change the price.
- Cheap compute is not the whole cost. The table compares compute only. With a session platform you also maintain the wait logic, banner handling, retries and storage code. With the API, repeat captures of the same URL can come from cache without a new render.
Our own prices are on the pricing page. Every API response also carries X-Credits-Used, so you can total real costs from your logs.
What this page does not measure yet
We have not yet run Browserbase and RenderScreenshot side by side on the same URL list with an account on each, so this version has no head-to-head latency, success rate or rendered image comparison. When we publish that run, it will use the same harness and report p50 and p95 for cold and warm captures. Until then, run it yourself: the vendor test script covers the API side and bench-cdp.mjs covers the session side.
Migrate a Browserbase screenshot job
Before: a typical Browserbase capture
// 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.setViewportSize({ width: 1280, height: 800 }); await page.goto(process.argv[2] ?? 'https://example.com', { waitUntil: 'networkidle' }); await page.locator('#onetrust-accept-btn-handler').click({ timeout: 2_000 }).catch(() => {}); await page.screenshot({ path: 'capture.png' }); } finally { await browser.close(); }
After: Node.js with the SDK
// capture.mjs: the Browserbase screenshot job, as one RenderScreenshot call. import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions, RenderScreenshotError } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const target = process.argv[2] ?? 'https://example.com'; try { const png = await client.take( TakeOptions.url(target) .width(1280) .height(800) .waitFor('networkidle') .blockCookieBanners() .format('png'), ); await writeFile('capture.png', png); console.log(`saved capture.png (${png.length} bytes)`); } catch (err) { if (err instanceof RenderScreenshotError) { console.error(`${err.code} (retryable: ${err.retryable}): ${err.message}`); } process.exitCode = 1; }
The hand-written banner click becomes blockCookieBanners(), which works across consent tools instead of one selector.
After: Python with plain HTTP
"""capture.py: the same capture with plain HTTP, no SDK needed.""" import os import sys import requests API = "https://api.renderscreenshot.com/v1/screenshot" target = sys.argv[1] if len(sys.argv) > 1 else "https://example.com" resp = requests.post( API, headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}"}, json={ "url": target, "viewport": {"width": 1280, "height": 800}, "wait": {"until": "networkidle"}, "block": {"cookie_banners": True}, "output": {"format": "png"}, }, timeout=60, ) if resp.status_code != 200: error = resp.json().get("error", {}) sys.exit(f"{error.get('code')} (retryable: {error.get('retryable')}): {error.get('message')}") with open("capture.png", "wb") as f: f.write(resp.content) print("saved capture.png, CDN copy:", resp.headers.get("X-Cache-URL"))
After: Ruby with the SDK
# capture.rb: the same capture with the Ruby SDK. require 'renderscreenshot' client = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) target = ARGV.fetch(0, 'https://example.com') options = RenderScreenshot::TakeOptions .url(target) .width(1280) .height(800) .wait_for('networkidle') .block_cookie_banners .format('png') begin File.binwrite('capture.png', client.take(options)) puts 'saved capture.png' rescue RenderScreenshot::Error => e warn "#{e.code} (retryable: #{e.retryable?}): #{e.message}" exit 1 end
The three "after" scripts were tested on 2026-10-15 against a local mock of the RenderScreenshot API (Node.js 22.18.0 with renderscreenshot 1.1.0, Python 3.14 with requests 2.34.2, Ruby 4.0.7 with the renderscreenshot gem 1.0.0), to check the request shape, file output and error path. The Python SDK is not on PyPI yet, which is why the Python version uses requests; see the Python SDK page for the GitHub install.
Response shape and auth differences
- What comes back. With Browserbase you get a session; the image is whatever your code writes. With RenderScreenshot the response body is the image (or JSON with
Accept: application/json), and headers carryX-Cache-URL(a public CDN link),X-Cache-Hit,X-Screenshot-Width,X-Screenshot-HeightandX-Credits-Used. See response headers. - Keys. Server-side calls use an
rs_live_key as a Bearer token. To put a capture straight into an<img>orog:imagetag, use a signed URL that expires (at most 30 days), so no key appears in your HTML. - Errors. Instead of reading a session recording, you get a typed error with a
codeand aretryableflag. Retry whenretryableis true; fix the request when it is false. - Concurrency. You no longer manage a pool of sessions. For many URLs at once, the batch endpoint takes a list and runs it within your plan's limits.
Roll out without a big switch
Put both providers behind one function and pick per job type, not per environment:
// capture-router.mjs (fragment): route by job type, keep sessions where they still earn their cost. export async function capture(job) { if (job.needsInteraction) return captureWithBrowserbase(job); // logins, clicks, agents return captureWithRenderScreenshot(job); // plain URL captures }
- Mark each job type as plain capture or interactive.
- Move one plain job type, compare outputs for a week, then move the next.
- Keep Browserbase for the interactive types, so the session bill covers only the work that needs a session.
When to stay with Browserbase
- Your captures are steps inside an AI agent's run, and the agent needs the session after the screenshot.
- You log in through a form with MFA or a CAPTCHA flow that you cannot replace with a cookie.
- You rely on session recordings and live view to debug customer issues.
- Your measured billed seconds per capture put you well below the break-even price in the table above, and you are happy to own the capture code.
Try it on your own URLs
Create a free key (150 credits a month, no credit card), run capture.mjs against the same URLs your Browserbase job captures today, and compare the files and your billed seconds side by side.