Browserless Alternative for Screenshots

A Browserless alternative for screenshots: browser infrastructure vs a screenshot API, cost per screenshot, and tested /screenshot migration code.

By Simon Chiu · Updated

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: token in 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-Used and X-Request-Id headers.
  • Errors. Browserless returns a non-200 with a message (for example 408 when waitForEvent never fires). Ours returns typed JSON errors with type, code and retryable (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.

Frequently asked questions

How does Browserless pricing work?
Browserless bills units. One unit is up to 30 seconds of browser time per connection, and each reconnect starts a new unit. As shown on its pricing page on October 14, 2026 (annual billing): Free 1,000 units, Prototyping $25 for 20,000, Starter $140 for 180,000, Scale $350 for 500,000, with overage from $0.0015 to $0.002 per unit.
Can Browserless take screenshots without Puppeteer code?
Yes. Its REST API has a POST /screenshot endpoint that takes a URL and Puppeteer-style options and returns the image. You still choose wait conditions and handle caching yourself.
Is a screenshot API cheaper than Browserless?
Usually not per screenshot. A simple capture is one Browserless unit, and units cost less than a RenderScreenshot credit on comparable plans. A screenshot API saves engineering time instead: caching, public links, presets, retries and wait logic are built in.
Should I move all my Browserless work to a screenshot API?
No. Keep Browserless for logins, multi-step flows, scraping and anything that needs a live browser session. Move only the jobs that are URL in, image out.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.