Disclosure: I build RenderScreenshot. Browserless is good infrastructure, and for most of what it does, it is the right tool. This page is about one slice of it: screenshots.
Browserless sells browser infrastructure: Chrome, Firefox and WebKit sessions that your Puppeteer or Playwright code connects to, plus REST endpoints for common jobs. RenderScreenshot sells a screenshot product: you describe the image, we run the browser, cache the result and give you a link. If your Browserless usage is mostly POST /screenshot calls or page.screenshot() at the end of a short script, a screenshot API can remove code. If it is logins, forms and scraping, keep Browserless. This page compares the two on cost per screenshot, maps the /screenshot options, and migrates those calls with a fallback in Node, Python and Ruby. For headless browser services side by side, see best headless browser APIs; for screenshot APIs, the best screenshot API comparison.
Last verified: October 15, 2026, against Browserless's screenshot API docs, request configuration docs and pricing page as published on October 14, 2026.
The short answer
| Keep Browserless if | Move the screenshots to RenderScreenshot if |
|---|---|
| Screenshots are one step inside logins, clicks and multi-page flows | The job is URL (or HTML) in, image out |
| You need Firefox or WebKit, stealth, captcha solving or residential proxies | You want caching and a public CDN link without building them |
| You batch many captures through one warm session, so units stay low | You want signed URLs in <img> and og:image tags |
| You already maintain the Puppeteer or Playwright code and it works | You want presets and wait options instead of wait code |
| You need self-hosting or a private deployment | You do not want to tune concurrency or session reuse |
Infrastructure vs product: what you still build
With a browser service, Browserless runs the browsers and you write everything around the capture. With a screenshot API, most of that list moves to the provider.
| Job | Browserless | RenderScreenshot |
|---|---|---|
| Launch, scale and clean up browsers | Browserless | RenderScreenshot |
| Choose and tune wait conditions | Your code (gotoOptions, waitForSelector, waitForFunction) |
Parameters (wait.until, wait.for_selector, wait.delay) |
| Hide cookie banners, ads, chat widgets | Your code, or request rules (rejectRequestPattern) |
block.cookie_banners, block.ads, block.chat_widgets |
| Cache repeated captures | You build it | Built in, 1 hour to 30 days, with a purge API |
| Store images and serve them publicly | You build it (S3, CDN) | X-Cache-URL, or upload to your bucket with storage |
| Public links that hide your key | You build a proxy | Signed URLs |
| Retries and error classes | Your code | Typed errors with a retryable flag |
| Logins, multi-step flows, scraping | Yes | No: cookies, headers and one click only |
Cost per screenshot
Browserless bills units: up to 30 seconds of browser time per connection, and every reconnect is a new unit. So the cost of a screenshot depends on how your code uses connections, not on the screenshot itself.
From the pricing pages on October 14, 2026 (Browserless at the annual-billing rates its page shows by default; monthly billing costs more):
| Plan | What you get | Cost per unit or credit |
|---|---|---|
| Browserless Prototyping | $25 for 20,000 units | $0.00125, overage $0.0020 |
| Browserless Starter | $140 for 180,000 units | $0.00078, overage $0.0017 |
| Browserless Scale | $350 for 500,000 units | $0.00070, overage $0.0015 |
| RenderScreenshot Starter | $10 for 2,500 credits | $0.0040 |
| RenderScreenshot Growth | $40 for 12,000 credits | $0.0033 |
| RenderScreenshot Scale | $100 for 60,000 credits | $0.0017 |
How many units is one screenshot? On my machine, a fresh Chromium plus one capture of example.com took 0.5 to 1 second (p50), and a capture in a reused browser about 0.2 to 0.3 seconds (method in the Puppeteer alternative). Add network time to a hosted browser and a simple REST capture is still far under 30 seconds: one unit. A heavy page that waits for networkidle0, scrolls for lazy images and takes 40 seconds is two units. A session that stays connected for 25 seconds and takes ten screenshots is one unit for ten shots.
So per screenshot, Browserless is cheaper than RenderScreenshot at every plan on this table, and much cheaper if you batch through warm sessions. The comparison that matters is the left column of the table above: the code and services you must build and run around those units. If that list is already built and paid for, stay. If it is not, the price gap buys it.
Option map for POST /screenshot
Browserless REST bodies mirror Puppeteer: options holds page.screenshot() options, gotoOptions holds page.goto() options, and a few wait and blocking fields sit at the top level.
| Browserless | RenderScreenshot (POST body) |
Notes |
|---|---|---|
?token= |
Authorization: Bearer rs_live_... |
|
url, html |
url, html |
|
viewport.width, viewport.height, viewport.deviceScaleFactor |
viewport.width, viewport.height, viewport.scale |
|
options.type (png, jpeg, webp) |
output.format |
We add AVIF and PDF |
options.quality |
output.quality |
|
options.fullPage |
capture.mode: "full_page" |
Add capture.scroll.simulate for lazy images |
options.clip |
capture.clip |
|
options.omitBackground |
output.transparent |
|
selector (top level) |
capture.mode: "element", capture.selector |
|
gotoOptions.waitUntil (load, domcontentloaded, networkidle0, networkidle2) |
wait.until (load, domcontentloaded, networkidle) |
Our networkidle means 500 ms with no requests, like networkidle0 |
gotoOptions.timeout |
wait.timeout (seconds, max 60) |
|
waitForSelector (selector, visible, hidden, timeout) |
wait.for_selector, wait.for_selector_mode |
|
waitForTimeout |
wait.delay (ms) |
|
waitForFunction, waitForEvent |
- | Use wait.for_selector on an element your page adds when ready |
addScriptTag, addStyleTag |
page.scripts, page.styles |
|
rejectResourceTypes |
block.resources |
|
rejectRequestPattern (regular expressions) |
block.requests (glob patterns) |
Rewrite each pattern |
bestAttempt: true |
- | We fail the request instead of returning a partial page |
stealth=true (launch parameter) |
- |
Head-to-head: what we measured
No hosted numbers for either service yet. Both need keys, and I have not run that benchmark. The local Puppeteer and Playwright timings above are the only first-hand numbers on this page, and they are a floor, not a head-to-head. To measure both services from your region, use the script in the best screenshot API comparison:
node compare.mjs --providers browserless,renderscreenshot --urls urls.txt --runs 3 --out bl-vs-rs.csv
Remember that Browserless has no image cache, so its "warm" rows measure a second render, while ours measure a cache hit.
Migration: move /screenshot calls, keep a fallback
This function switches screenshot jobs to RenderScreenshot with RS_ROLLOUT_PERCENT=100. While you watch the switch, errors that our API marks as retryable (timeouts, render failures, rate limits) fall back to Browserless, so a bad hour on our side does not become a bad hour for your users. Remove the fallback once the error log is quiet.
Node.js
// screenshot.mjs: move /screenshot calls from Browserless to RenderScreenshot, with a fallback. // Node 22+, npm i renderscreenshot. import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions, RenderScreenshotError } from 'renderscreenshot'; const rs = new Client(process.env.RS_API_KEY); const USE_RS = process.env.RS_ROLLOUT_PERCENT === '100'; async function viaBrowserless({ url, width, height, fullPage }) { const endpoint = `https://production-sfo.browserless.io/screenshot?token=${encodeURIComponent(process.env.BROWSERLESS_TOKEN)}`; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, viewport: { width, height }, gotoOptions: { waitUntil: 'networkidle2' }, options: { type: 'png', fullPage }, }), signal: AbortSignal.timeout(60_000), }); if (!res.ok) throw new Error(`Browserless ${res.status}: ${await res.text()}`); return Buffer.from(await res.arrayBuffer()); } function viaRenderScreenshot({ url, width, height, fullPage }) { let o = TakeOptions.url(url).width(width).height(height).format('png').waitFor('networkidle'); if (fullPage) o = o.fullPage(); return rs.take(o); } export async function screenshot(input) { const job = { width: 1280, height: 800, fullPage: false, ...input }; if (!USE_RS) return viaBrowserless(job); try { return await viaRenderScreenshot(job); } catch (err) { // During the switch, keep Browserless as the safety net for errors worth retrying. if (err instanceof RenderScreenshotError && err.retryable) { console.warn(`fallback to Browserless: ${err.code}`); return viaBrowserless(job); } throw err; } } if (import.meta.url === `file://${process.argv[1]}`) { const image = await screenshot({ url: process.argv[2] ?? 'https://example.com/' }); await writeFile('out.png', image); console.log(`out.png: ${image.length} bytes`); }
Python
# screenshot.py: move /screenshot calls from Browserless to RenderScreenshot, with a fallback. # Python 3.9+. pip install httpx git+https://github.com/Render-Screenshot/rs-python.git import os import sys import httpx from renderscreenshot import Client, RenderScreenshotError, TakeOptions rs = Client(os.environ['RS_API_KEY']) USE_RS = os.environ.get('RS_ROLLOUT_PERCENT') == '100' def via_browserless(url, width=1280, height=800, full_page=False): res = httpx.post( 'https://production-sfo.browserless.io/screenshot', params={'token': os.environ['BROWSERLESS_TOKEN']}, json={ 'url': url, 'viewport': {'width': width, 'height': height}, 'gotoOptions': {'waitUntil': 'networkidle2'}, 'options': {'type': 'png', 'fullPage': full_page}, }, timeout=60, ) res.raise_for_status() return res.content def via_renderscreenshot(url, width=1280, height=800, full_page=False): options = TakeOptions.url(url).width(width).height(height).format('png').wait_for('networkidle') if full_page: options = options.full_page() return rs.take(options) def screenshot(url, **kwargs): if not USE_RS: return via_browserless(url, **kwargs) try: return via_renderscreenshot(url, **kwargs) except RenderScreenshotError as err: if not err.retryable: raise print(f'fallback to Browserless: {err.code}', file=sys.stderr) return via_browserless(url, **kwargs) if __name__ == '__main__': image = screenshot(sys.argv[1] if len(sys.argv) > 1 else 'https://example.com/') with open('out.png', 'wb') as f: f.write(image) print(f'out.png: {len(image)} bytes')
Ruby
# screenshot.rb: move /screenshot calls from Browserless to RenderScreenshot, with a fallback. # Ruby 3.0+. gem install renderscreenshot require 'json' require 'net/http' require 'renderscreenshot' RS = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) USE_RS = ENV['RS_ROLLOUT_PERCENT'] == '100' def via_browserless(url, width: 1280, height: 800, full_page: false) uri = URI('https://production-sfo.browserless.io/screenshot') uri.query = URI.encode_www_form(token: ENV.fetch('BROWSERLESS_TOKEN')) req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') req.body = { url: url, viewport: { width: width, height: height }, gotoOptions: { waitUntil: 'networkidle2' }, options: { type: 'png', fullPage: full_page } }.to_json res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 60) { |http| http.request(req) } raise "Browserless #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess) res.body end def via_renderscreenshot(url, width: 1280, height: 800, full_page: false) options = RenderScreenshot::TakeOptions.url(url).width(width).height(height).format('png').wait_for('networkidle') options = options.full_page if full_page RS.take(options) end def screenshot(url, **kwargs) return via_browserless(url, **kwargs) unless USE_RS begin via_renderscreenshot(url, **kwargs) rescue RenderScreenshot::Error => e raise unless e.retryable? warn "fallback to Browserless: #{e.code}" via_browserless(url, **kwargs) end end if $PROGRAM_NAME == __FILE__ image = screenshot(ARGV[0] || 'https://example.com/') File.binwrite('out.png', image) puts "out.png: #{image.bytesize} bytes" end
Tested on October 15, 2026 against a local mock of both APIs, at RS_ROLLOUT_PERCENT=0 and 100, and with a target URL that makes the mock return a retryable 500 from RenderScreenshot: in all three languages the call fell back to Browserless and still wrote the image. Versions: Node.js 22.18.0 with renderscreenshot 1.1.0, Python 3.14 with the rs-python SDK 1.0.0 and httpx 0.28.1, Ruby 4.0.7 with the renderscreenshot gem 1.0.0.
If you use puppeteer.connect() instead of REST
If your screenshots come from Puppeteer code connected to Browserless (browserWSEndpoint), the change is the same as replacing self-hosted Puppeteer: one function that opens a page, waits and captures becomes one API call. The Puppeteer alternative page shows both versions side by side. Keep the connect() code for every script that does more than capture.
Response and auth differences
- Auth. Browserless:
tokenin the query string. RenderScreenshot: Bearer header for server calls, signed URLs for public HTML. - Response. Both return image bytes. Ours adds
X-Cache-URL,X-Cache-Hit,X-Credits-UsedandX-Request-Idheaders. - Errors. Browserless returns a non-200 with a message (for example 408 when
waitForEventnever fires). Ours returns typed JSON errors withtype,codeandretryable(error codes). - Concurrency. Browserless limits concurrent browsers by plan (2 to 100, plus add-ons). RenderScreenshot limits requests per minute and concurrent renders by plan (rate limits).
When to stay with Browserless
- Your screenshots are part of longer automation. Logging in, filling forms and paging through results need a live session.
- You need Firefox or WebKit, stealth mode, captcha solving or residential proxies.
- Cost per unit decides it and you already have caching, storage and public links built.
- You self-host or need a private deployment for compliance.
Try the switch on one job
Create a free RenderScreenshot key, pick the one Browserless job that only takes screenshots, and run it through the function above at RS_ROLLOUT_PERCENT=100 in staging. The free plan has 150 credits a month and needs no credit card.
Related
- Best headless browser APIs: Browserless next to other browser services.
- Cloudflare Browser Rendering alternative: the other "browser as infrastructure" option.
- Scaling Puppeteer screenshots: the work Browserless saves you, in detail.
- Wait parameters:
until,delay,for_selectorandtimeout.