Browserless vs Cloudflare Browser Rendering

Browserless vs Cloudflare Browser Rendering for screenshots: where the browser runs, limits, cost at 1k to 100k captures, code, and production lessons.

By Simon Chiu · Updated

Disclosure: I build RenderScreenshot, and our renderer runs on Cloudflare Browser Rendering. That makes me a Cloudflare customer with production experience, and a Browserless outsider. I have tried to keep the two sides equal: product facts come from each vendor's docs and pricing pages (read on 2026-10-15), and the experience notes are labelled as ours.

Short answer: pick Cloudflare Browser Rendering when your capture code can live in a Cloudflare Worker, your output goes to R2 or another Cloudflare product, and you want to pay for browser time by the hour. Pick Browserless when your code runs on your own servers in any language, you want a standard puppeteer.connect() or Playwright endpoint, you need Firefox or WebKit, or you may self-host later. Both offer REST endpoints for one-shot screenshots, so neither requires browser code for the simple case.

Cloudflare's docs now call the product Browser Run, and older URLs redirect there. This page uses the original name because that is what most people search for. Other pairs are in the comparison hub.

Where the browser runs, and where your code runs

This is the decision under all the others.

  • Cloudflare: the browser runs in Cloudflare's network. Your automation code runs in a Worker that is bound to the browser (browser = { binding = "MYBROWSER" }), using @cloudflare/puppeteer, Cloudflare's fork of Puppeteer, or Playwright. The Worker and the browser are close, so each command is a short hop. From outside Cloudflare you use the REST endpoints.
  • Browserless: the browser runs in a Browserless region (San Francisco, London or Amsterdam on the shared cloud), or in your own Docker deployment. Your code runs anywhere and connects over a WebSocket. Every Puppeteer command crosses the network between your server and that region.

If your app is a Rails, Django or Laravel monolith, Browserless fits without new infrastructure. If you already deploy Workers, Cloudflare removes the network hop and the second vendor.

Side by side

Browserless (shared cloud) Cloudflare Browser Rendering
Control API Puppeteer, Playwright, BrowserQL over WebSocket @cloudflare/puppeteer or Playwright inside a Worker; CDP access
One-shot REST /screenshot, /pdf, /content, /scrape, /function /screenshot, /pdf, /content, /markdown, /links, /scrape, /snapshot
Auth Token in the URL query string Cloudflare API token (REST) or the Worker binding
Browsers Chrome, WebKit, Firefox Chromium
Billing Units: up to 30 s of browser time per connection Browser hours; plus concurrent browsers for Puppeteer/Playwright sessions
Concurrency (entry paid plan) 10 (+5) on Prototyping 10 averaged per month included, up to 200 per account on Workers Paid
New-browser rate limit Not stated on the pricing page 3 new browsers per second on Workers Paid; 1 every 20 s on Free
Idle timeout Max session time by plan (2 to 60 min) 60 s of inactivity by default, keep_alive up to 20 min
Bot identification Stealth and CAPTCHA features listed Cloudflare states requests are always identified as a bot
Self-hosting Docker or licensed self-hosting No

Sources: Browserless pricing, Browserless docs, Cloudflare pricing, Cloudflare limits, Cloudflare screenshot endpoint.

The bot identification row matters for screenshots of sites behind bot protection. If your targets block automated traffic, test them on both before you commit.

The same screenshot on each

Cloudflare: a Worker that captures and stores to R2

wrangler.toml:

name = "capture-worker"
main = "src/index.ts"
compatibility_date = "2026-10-01"

browser = { binding = "MYBROWSER" }

[[r2_buckets]]
binding = "SHOTS"
bucket_name = "screenshots"

src/index.ts:

import puppeteer from '@cloudflare/puppeteer';

interface Env {
  MYBROWSER: Fetcher;
  SHOTS: R2Bucket;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const target = new URL(request.url).searchParams.get('url');
    if (!target || !/^https?:\/\//.test(target)) {
      return new Response('Pass ?url=https://...', { status: 400 });
    }

    const browser = await puppeteer.launch(env.MYBROWSER);
    try {
      const page = await browser.newPage();
      await page.setViewport({ width: 1280, height: 800 });
      await page.goto(target, { waitUntil: 'networkidle2', timeout: 30_000 });
      const png = await page.screenshot({ type: 'png' });

      const key = `shots/${crypto.randomUUID()}.png`;
      await env.SHOTS.put(key, png, { httpMetadata: { contentType: 'image/png' } });
      return Response.json({ key, bytes: png.byteLength });
    } finally {
      await browser.close(); // stops browser-hour billing for this session
    }
  },
};

Install with npm i @cloudflare/[email protected] and deploy with wrangler deploy. In production, put an auth check in front of this Worker: as written, anyone who finds the URL can spend your browser hours.

Or the REST endpoint, with no Worker:

curl -X POST \
  "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/browser-rendering/screenshot" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "viewport": {"width": 1280, "height": 800}, "screenshotOptions": {"type": "png"}}' \
  --output cloudflare.png

Cloudflare's docs show this path under both /browser-rendering/ and the newer /browser-run/. Do not send quality with PNG: the endpoint returns a 400.

Browserless: connect from any Node server

import puppeteer from 'puppeteer-core';
import { writeFile } from 'node:fs/promises';

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_TOKEN}`,
});
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });
  await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 30_000 });
  await writeFile('browserless.png', await page.screenshot({ type: 'png' }));
} finally {
  await browser.close();
}

Browserless also lists London and Amsterdam endpoints; use the hostname shown in your account. When navigation dominates the time, pick the region closest to the sites you capture, not to your server. The REST route works the same way as Cloudflare's: POST /screenshot?token=... with {"url": "..."}.

What it costs at three volumes

Assume one capture keeps a browser busy for 3 seconds and uses one connection. In our local tests a viewport capture took 1.5 to 1.9 seconds from launch to close, so 3 seconds leaves room for network round trips. It is an assumption, not a measurement of either service.

Captures per month Browser time Cloudflare (Workers Paid) Browserless
1,000 0.8 h Inside the 10 included hours Free plan: 1k units
10,000 8.3 h Inside the 10 included hours Prototyping: 20k units, $25/mo billed annually
100,000 83 h 73 extra hours x $0.09 = about $6.57, plus concurrency above 10 Starter: 180k units, $140/mo billed annually

Prices as listed on each vendor's pricing page on 2026-10-15. The Cloudflare column excludes the Workers Paid subscription itself and Worker request costs.

The concurrency line is the one to model carefully on Cloudflare. It is the monthly average of your daily peak, and each browser above 10 costs $2.00. A daily burst to 30 browsers, every day, adds 20 x $2.00 = $40 to the month, far more than the browser hours. On Browserless, concurrency is a plan limit, not a meter, so a burst queues or fails instead of adding cost.

What we learned running screenshots on Cloudflare

These notes are from operating RenderScreenshot's renderer. The full write-up, with code, is in our Cloudflare Browser Rendering tutorial.

  • Reuse sessions. A fresh launch takes seconds; connecting to a warm session is much faster. @cloudflare/puppeteer exposes sessions() and connect(), and browser.disconnect() leaves the session alive for the next request. This also keeps your concurrency peak down.
  • The launch-rate limit arrives before the concurrency limit. When traffic bursts, new-browser launches per second are the first thing to fail, with a plain HTTP 429. Log which limit you hit.
  • Warm sessions can vanish. A session you cached can close between requests. Treat connect() as fallible and fall back to launch().
  • For one-shot captures, the REST endpoint holds up well. We expected the binding to be faster for simple screenshots; in practice the REST path was competitive. The binding wins for multi-step flows.

We do not run Browserless in production, so there is no equivalent list for it here. Its Docker image is the obvious way to try it locally before you buy; our guide to self-hosting Browserless with Docker covers that.

How to benchmark the two fairly

We have not published head-to-head timings for this pair yet. If you run your own:

  1. Compare like with like: REST against REST, or a Worker using the binding against a Node server using puppeteer.connect() from a region near the Browserless endpoint.
  2. Measure session setup, navigation and capture separately. Setup is the vendor; navigation is mostly the target site.
  3. Run at least 5 captures per URL, cold (new session) and warm (reused session), and report median and p95.
  4. Include a page behind bot protection and a JavaScript-heavy page, not only example.com.
  5. Record the 429 rate during a burst test. Limits differ more than speed does.

The harness on our Browserbase vs Browserless page times setup, navigation and capture per run and accepts any browserWSEndpoint, so you can point it at Browserless directly. For Cloudflare, put the same timing calls inside a Worker.

Decision table

Situation Choose
Your stack already runs on Workers and R2 Cloudflare
Your capture code runs on your own servers (any language) Browserless
You need Firefox or WebKit rendering Browserless
Steady, predictable traffic and short captures Cloudflare (browser hours stay low)
Large daily bursts Browserless, or Cloudflare with session reuse and a queue to cap the peak
Data must stay in your own infrastructure Browserless self-hosted
You only need URL-to-image and do not want to write browser code Either REST endpoint, or a screenshot API

If you only need the image

Both products still leave caching, signed URLs, cookie-banner handling and retries to you. RenderScreenshot does those per request, on top of the same Cloudflare browser infrastructure we described above:

curl -o example.png \
  -H "Authorization: Bearer $RS_API_KEY" \
  "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&width=1280&height=800&block_cookies=true"

Repeat requests are served from cache, and the response includes an X-Cache-URL CDN link you can store (see response headers). Get a free API key to compare it with your Worker or Browserless script on the same URLs.

Last verified: 2026-10-15 (docs, limits and pricing). Benchmark pending.

Frequently asked questions

Is Cloudflare Browser Rendering cheaper than Browserless?
For short screenshots, usually yes on browser time, because Cloudflare bills browser hours and Browserless bills a unit per connection of up to 30 seconds. Cloudflare also charges for concurrent browsers above the included amount, which can dominate with bursty traffic.
Can I use Cloudflare Browser Rendering without writing a Worker?
Yes. Cloudflare's REST endpoints (it calls them Quick Actions) take a URL in a JSON body and return a screenshot, PDF, HTML or Markdown.
Can I connect my existing Puppeteer code to Cloudflare like Browserless?
The main path is the Workers binding with Cloudflare's fork, @cloudflare/puppeteer, so your capture code runs inside a Worker. Browserless accepts a standard puppeteer.connect from any server.
Why do I get HTTP 429 from Cloudflare Browser Rendering?
You hit a per-account limit: concurrent browsers or the rate of new browser launches. Back off on 429 and reuse sessions instead of launching one per request.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.