Cloudflare Browser Rendering, renamed Browser Run, gives you headless Chrome on Cloudflare’s network. You control it with a Worker, its REST API, or a Puppeteer or Playwright script, without running a browser yourself.
In my tests, cold starts were fast. The problem appeared on the second run, when a new browser started without the first run’s cookies. Every request I sent also included headers that identify it as Cloudflare automation.
This guide measures Browser Run’s limits. It also shows what to use when a site must recognize you as the same returning user.
TL;DR
- Cold starts are fast. New browsers loaded a page in 0.7 to 3.2 seconds across 20 cold runs from a Worker in Marseille.
- Each new browser starts empty. A new session had none of the previous run’s cookies or localStorage.
- Every request identifies itself as automation. Standard-pool sessions ran headless Chrome 128 from Cloudflare IPs, sent signed headers that you can’t remove, and had no proxy option.
- Pick by workload. Browser Run suits screenshots, PDFs, and crawls that sites allow. For signed-in workflows on accounts you control, use a profile with its own cookies, fingerprint, and proxy.
Cloudflare Browser Rendering vs Gologin Cloud Browser
Rows marked “measured” come from my runs. The rest come from each vendor’s documentation:
| Cloudflare Browser Rendering (Browser Run) | Gologin Cloud Browser | |
| Connection method | Workers binding, REST Quick Actions, or CDP | One CDP WebSocket URL for Playwright, Puppeteer, or other CDP clients |
| Detected browser and OS (measured) | Headless Chrome 128 on Linux (standard pool) | Chrome on Linux, from the profile’s fingerprint |
| Persistence between runs (measured) | Nothing, unless you reuse the open browser’s default context or restore saved storageState |
Cookies and localStorage from earlier runs |
| Device fingerprint across runs (measured) | The same stock headless device (800 x 600, SwiftShader, UTC) across all 3 sessions of my fingerprint test | The profile’s own device, identical in 10 of 10 runs |
| Egress IP | Cloudflare IP ranges, no proxy setting | The profile’s residential, mobile, datacenter, or dedicated ISP proxy |
| Automation signals (measured) | Cf-Brapi-Devtools and Web Bot Auth headers, navigator.webdriver is true |
Only standard Chrome headers, navigator.webdriver is false |
| Cold start to loaded page (measured from the same machine) | Median 2.1 s, range 1.4 to 2.5 s | Median 7.4 s, range 6.6 to 8.8 s |
| Session length | No fixed maximum while active | As long as the job needs |
What is Cloudflare Browser Rendering?
Cloudflare Browser Rendering is a serverless browser rendering service. Your code requests a browser. Cloudflare starts headless Chrome in a data center near the caller and bills for the time it runs. Teams use it for screenshots, PDFs, and Cloudflare Workers web scraping of pages they’re allowed to collect.
Cloudflare renamed it Browser Run and moved it to Cloudflare Containers. You can control it in three main ways:
- Quick Actions. REST endpoints such as
/content,/screenshot, and/pdfload a URL and return the result in one call. - Browser Sessions from a Worker. A browser binding works with the Cloudflare Puppeteer fork (
@cloudflare/puppeteer) or Playwright fork (@cloudflare/playwright). - The CDP endpoint. Chrome DevTools Protocol (CDP) clients can connect from outside Cloudflare with an API token.
A Worker needs one binding in wrangler.jsonc, "browser": { "binding": "BROWSER" }, plus the nodejs_compat flag and a compatibility_date of 2025-09-15 or later. The Puppeteer fork split a launch into two calls, which I timed separately:
import puppeteer from '@cloudflare/puppeteer';
export default {
async fetch(request, env) {
// Starts a new browser and returns its session ID
const { sessionId } = await puppeteer.acquire(env.BROWSER, { keep_alive: 120_000 });
// Opens the CDP connection to that session
const browser = await puppeteer.connect(env.BROWSER, sessionId);
try {
const page = await browser.newPage();
await page.goto('https://books.toscrape.com/', { waitUntil: 'load' });
return Response.json({ version: await browser.version(), title: await page.title() });
} finally {
await browser.close();
}
},
};
Deployed without changes, the Worker returned this response:
{"version":"Chrome/128.0.6613.137","title":"All products | Books to Scrape - Sandbox"}
Every standard-pool session I tested reported Chrome 128.
Cloudflare Browser Rendering benchmark and limits
I deployed a benchmark Worker on a Workers Free account, so the setup costs nothing to reproduce. The Worker ran in Cloudflare’s Marseille (MRS) data center. It loaded example.com, books.toscrape.com, quotes.toscrape.com/js, and a Wikipedia article.
Cold starts and response latency
A cold run acquires a new browser, connects over CDP, opens a new page, and loads the target URL. Five cold runs per target produced these results:
| Target (page weight) | Acquire, median | Total time, median | Total time, range |
| example.com (2 KB) | 470 ms | 1,233 ms | 660 to 1,463 ms |
| books.toscrape.com (330 KB) | 394 ms | 2,202 ms | 2,065 to 3,175 ms |
| quotes.toscrape.com/js (100 KB) | 521 ms | 2,265 ms | 1,471 to 2,338 ms |
| Wikipedia article (414 KB) | 347 ms | 840 ms | 761 to 1,300 ms |
A Worker in the same data center measured these times. From my own machine, the median cold start to a loaded page was 2.1 seconds.
The terminal output shows each run:
On books.toscrape.com and quotes.toscrape.com/js, loading took longer than browser startup in 7 of 10 runs.
Cloudflare’s dashboard listed each session in its Runs tab:

Normal Closure means the code called browser.close(), so no session waited for the idle timeout.
In 15 paired runs, a reused live session loaded example.com in a median of 242 ms. A cold start took a median of 997 ms. Each reuse opened a new browser context, as Cloudflare’s Reuse sessions guide shows, and avoided the rate limit on new browsers.
Idle timeout, keep_alive, and rate limits
Cloudflare’s docs said that sessions closed after 60 seconds without a CDP command. In my idle test, the browser disconnected 68 seconds after its last command, and puppeteer.history() recorded the close reason as BrowserIdle. The next call failed with Attempted to use detached Frame, which doesn’t mention a timeout:
Check puppeteer.history() before you debug selectors.
For longer pauses between commands, pass keep_alive at launch. The limits page said keep_alive extended the idle timeout to 10 minutes, but the Browser binding API page listed 20 minutes for its keepAlive option. The API accepted 1,200,000 ms (20 minutes) and rejected 1,200,001 ms with Too big: expected number to be <=1200000.
Waiting for the browser doesn’t count as Worker CPU time, but some invocations have a wall-time limit. Cron Triggers, Queue consumers, and Durable Object alarms stop after 15 minutes, so split long crawls into queued batches.
Workers Free allowed 1 new browser every 20 seconds. When I requested four browsers at the same time, one started and three got HTTP 429 Rate limit exceeded with Retry-After: 20. For Workers Paid, the limits page listed defaults of 200 concurrent browsers and 3 new browsers per second.
Cloudflare’s docs showed a handler that reads error.status and a Retry-After header, but the Puppeteer fork threw a plain Error with neither property. The error message was Unable to create new browser: code: 429: message: Rate limit exceeded. When I exhausted my Free account’s daily quota, launches failed with that same message, not the documented Browser time limit exceeded for today.
Only the raw response’s Retry-After differed, and it showed the time remaining until midnight UTC. puppeteer.limits() reported no wait in both cases, so keep the attempt count low:
import puppeteer from '@cloudflare/puppeteer';
// Retries a rate-limited launch. When the daily quota is exhausted, launches
// fail with the same message, so a low attempt count limits the wait.
export async function launchWithRetry(env, attempts = 3) {
for (let i = 1; ; i++) {
try {
return await puppeteer.launch(env.BROWSER, { keep_alive: 600_000 });
} catch (err) {
const rateLimited = /code: 429: message: Rate limit exceeded/.test(String(err));
if (!rateLimited || i === attempts) throw err;
await new Promise((resolve) => setTimeout(resolve, 20_000 * i));
}
}
}
Immediately after another browser launched, the helper got a 429, waited 20 seconds, and returned a browser on its second attempt:
error.status is undefined, so the helper matches the message.
Session and fingerprint persistence
Run 1 set a cookie and a localStorage key on example.com, set a server-side cookie on httpbin.org, and then closed the browser. Run 2 used a new browser and found none of them. When run 1 left its browser open instead, reconnecting kept everything in the default context, and the IP stayed the same. A new browser context in that session, the kind that the Reuse sessions guide opens, started empty.
The Playwright fork’s launchPersistentContext threw Cloudflare Workers does not support browserType.launchPersistentContext. The Playwright guide showed the documented alternative, which saves storageState to Workers KV and passes it to newContext() on the next run.
In my test, storageState restored the cookie and localStorage value but included no device fingerprint. Every session in my fingerprint test reported Chrome 128 on Linux and SwiftShader software rendering. Each also had an 800 x 600 screen, the UTC timezone, and navigator.webdriver set to true.
Treat a Browser Run session as disposable, and treat login state as data that you save and restore yourself.
Proxy and egress identity
Browser Run had no proxy setting and no IP rotation option in my tests. Setting proxyServer on a browser context failed with proxyServer and proxyBypassList parameters are not allowed. Cloudflare’s FAQ states that “All rendering traffic comes from Cloudflare IP ranges”. My four new sessions used four different egress IPs in AS13335 (Cloudflare, Inc.), all geolocated in France.
Every request I captured included Cf-Brapi-Devtools: 1 plus Signature, Signature-Input, and Signature-Agent headers. These signature headers implement Web Bot Auth, an Ed25519 signature that proves the request came from Browser Run. Cloudflare’s automatic request headers page says these headers “cannot be removed or overridden”. Browser Run’s own screenshot of httpbin.org/anything shows what the site received:
Browser Run took this screenshot in its default 800 x 600 viewport.
The TLS certificates suggested how Browser Run added those headers to HTTPS traffic. Inside the session, the certificates that Chrome received for example.com, books.toscrape.com, and Wikipedia had no issuer name and the same one-year validity period. That pattern indicated a TLS-terminating proxy.
Two TLS fingerprinting services received a non-Chrome handshake. The tls.peet.ws service reported HTTP/1.1 with no ALPN, and tls.browserleaks.com reported JA4 t13d181000_796efca44d18_991f053334f2:
Empty akamai fields mean no HTTP/2 fingerprint.
Cloudflare’s docs say “Browser Run does not bypass CAPTCHAs, Turnstile challenges, or any other bot protection mechanisms”. Its FAQ adds that Browser Run requests “are always identified as bot traffic by Cloudflare”. Allowlisting Browser Run on your own website requires an Enterprise plan.
Browser Run works well for rendering your own pages and crawling sites that allow it. It works poorly for a workflow that must return to a third-party site as the same signed-in user.
Cloudflare Browser Rendering pricing and monthly cost examples
When I tested, the pricing page listed these rates on top of the $5 monthly Workers Paid plan:
| Workers Free | Workers Paid | |
| Browser hours | 10 minutes per day | 10 hours per month, then $0.09 per hour |
| Concurrent browsers (Browser Sessions) | 3 | 10 included (monthly average), then $2.00 per extra browser |
puppeteer.history() recorded a median of 0.75 seconds of browser time per Wikipedia page load, so 100,000 loads a month come to about 21 browser hours. I assumed the same browser time per page on Gologin. Its Professional plan was the cheapest fit, at $4.50 a month with 50 browser hours, 1 parallel session, and $2 per extra session. I estimated these monthly costs with Cloudflare’s rounding to the nearest hour:
| Workload | Service and plan | Browser hours | Parallel sessions fee | Total per month |
| 100,000 Wikipedia page loads, 10 browsers at peak every day | Browser Run, Workers Paid ($5) | 21 (11 billed at $0.09) | $0 (10 included) | $5.99 |
| 100,000 Wikipedia page loads, 10 browsers at peak every day | Gologin, Professional ($4.50) | 21 (50 included) | $18.00 (9 extra × $2) | $22.50 |
| 100,000 Wikipedia page loads, 50 browsers at peak every day | Browser Run, Workers Paid ($5) | 21 (11 billed at $0.09) | $80.00 (40 extra × $2) | $85.99 |
| 100,000 Wikipedia page loads, 50 browsers at peak every day | Gologin, Professional ($4.50) | 21 (50 included) | $98.00 (49 extra × $2) | $102.50 |
Browser time cost little at these volumes.
Gologin Cloud Browser: one CDP endpoint with a persistent identity
The cloud browser treats the profile as the identity and the browser as disposable. Cookies, localStorage, the device fingerprint, and the proxy are stored on the profile and load on every connection. Playwright and Puppeteer connect through one WebSocket URL. So a script for Browser Run’s CDP endpoint keeps its automation logic and mainly changes the connection call.
The user agent, screen, WebGL renderer, canvas, and fonts stayed the same in every run. Sessions run Orbita, a Chromium build with fingerprint control built into the engine.
Proxies attach to the profile with one API call. The proxy network includes residential, mobile, datacenter, and dedicated ISP IPs.
I ran the same persistence and fingerprint tests 10 times against one saved profile. Odd-numbered runs wrote a new cookie and localStorage value:
| Signal a site reads | Browser Run, run 2 in a new session | Saved cloud profile, 10 runs |
| Cookie written by an earlier run | Missing | Present in all 9 later runs |
| localStorage written by an earlier run | Missing | Present in all 9 later runs |
| Cookie received by httpbin.org | None | Present in all 9 later runs |
| Device fingerprint hash | The same stock headless device in all 3 test sessions | 043d6e145e54d603 in 10 of 10 runs |
| navigator.webdriver | true | false |
| TLS handshake at the site | Non-Chrome handshake over HTTP/1.1 | Chrome handshake over HTTP/2 |
In Live View, the profile’s second session still sent the cookie that its first session set:
The profile persisted the cookie without any save or restore code.
Cloud browser pricing is flat and hourly, and the pricing section prices both workloads. The cloud browser page has current rates and a cost calculator. The free tier includes saved profiles.
Connect Playwright to a saved profile over CDP
You need Node.js 22 or later, playwright-core (npm install playwright-core), an API key from the dashboard, and a profile ID. One API call creates a profile with its own Linux device fingerprint:
curl -s -X POST https://api.gologin.com/browser/quick \
-H "Authorization: Bearer $GL_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "returning-user-1", "os": "lin"}'
The response includes the profile’s id, which you set as the GL_PROFILE_ID environment variable. The connection URL and stop call follow the Getting Started guide. The script also counts visits in the profile’s localStorage, reads every product card, and prints the canvas hash that a site reads:
// Save this file as extract.mjs so that Node loads it as an ES module
import { chromium } from 'playwright-core';
const TOKEN = process.env.GL_API_TOKEN;
const PROFILE_ID = process.env.GL_PROFILE_ID;
const CDP_URL = `wss://cloudbrowser.gologin.com/connect?token=${TOKEN}&profile=${PROFILE_ID}`;
async function run() {
let browser;
try {
browser = await chromium.connectOverCDP(CDP_URL, { timeout: 60_000 });
// The profile's saved cookies and storage are in its default context, so open pages there
const context = browser.contexts()[0];
// The profile restores tabs from its last session, so work in a new one
const page = await context.newPage();
await page.goto('https://books.toscrape.com/', { waitUntil: 'domcontentloaded' });
// localStorage is saved with the profile, so this count persists between sessions
const visit = await page.evaluate(() => {
const count = Number(localStorage.getItem('visits') ?? 0) + 1;
localStorage.setItem('visits', String(count));
return count;
});
// The device fingerprint is saved with the profile too, so a site reads the same canvas hash every run
const canvas = await page.evaluate(async () => {
const ctx = document.createElement('canvas').getContext('2d');
ctx.font = '16px Arial';
ctx.fillText('Gologin profile', 4, 20);
const bytes = new TextEncoder().encode(ctx.canvas.toDataURL());
const digest = await crypto.subtle.digest('SHA-256', bytes);
return Array.from(new Uint8Array(digest).slice(0, 8), (b) => b.toString(16).padStart(2, '0')).join('');
});
// One evaluate call returns every card in a single CDP round trip
const books = await page.evaluate(() =>
Array.from(document.querySelectorAll('article.product_pod'), (card) => ({
title: card.querySelector('h3 a')?.getAttribute('title'),
price: card.querySelector('.price_color')?.textContent.trim(),
})),
);
console.log(`Profile visit ${visit}: extracted ${books.length} books`, books[0]);
console.log(`Canvas hash a site reads: ${canvas}`);
} finally {
// Stop the cloud session on every code path, so it ends when the job does
await browser?.close();
const res = await fetch(`https://api.gologin.com/browser/${PROFILE_ID}/web`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${TOKEN}` },
});
console.log(`Stopped cloud session: HTTP ${res.status}`);
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Two runs of node extract.mjs against a new profile printed:
Profile visit 1: extracted 20 books { title: 'A Light in the Attic', price: '£51.77' }
Canvas hash a site reads: 5227a72003867602
Stopped cloud session: HTTP 204
Profile visit 2: extracted 20 books { title: 'A Light in the Attic', price: '£51.77' }
Canvas hash a site reads: 5227a72003867602
Stopped cloud session: HTTP 204
While a session runs, the dashboard lists it with View and Stop controls:
The View button opens Live View.
Final thoughts
Cloudflare Browser Rendering is a fast way to render pages, take screenshots, and crawl sites that allow it. In my tests, each new session started empty and identified itself as Cloudflare automation, so Browser Run’s main limits were about identity, not speed. For workflows that must stay signed in, use a browser that stores cookies, fingerprint, and proxy on a profile.
Gologin Cloud Browser works with the connectOverCDP call that your Playwright code already uses. Create one profile on the free tier, run the script twice, and check that the second run prints visit 2 and the same canvas hash.
FAQ
Is Cloudflare Browser Rendering free?
Partly. When I tested, Cloudflare listed 10 minutes of browser time per day and 3 concurrent browsers on Workers Free. Workers Paid cost $5 a month, included 10 browser hours, and billed $0.09 for each extra hour. Cloudflare’s pricing page lists the current rates.
Does Cloudflare Browser Rendering support Playwright?
Yes. Cloudflare maintains a Playwright fork for Workers, and standard Playwright clients can connect to Browser Run’s CDP endpoint with an API token. In my tests, the fork didn’t support persistent contexts, so you save and restore login state yourself, for example as storageState in Workers KV.
Does Cloudflare Browser Rendering pass bot checks?
No, by design. Cloudflare says that Browser Run does not bypass CAPTCHAs, Turnstile, or other bot protection, and its requests are always identified as bot traffic. Every request that I captured included a signed Web Bot Auth header and came from a Cloudflare IP.
What are Cloudflare Browser Rendering limits?
When I tested, the default idle timeout was 60 seconds. Cloudflare’s docs listed 10 or 20 minutes as the keep_alive maximum, and the API accepted 20 minutes. Documented Workers Paid defaults were 200 concurrent browsers and 3 new browsers per second. Workers Free allowed 3 concurrent browsers and 1 new browser every 20 seconds.
When should I use Gologin instead of Browser Run?
Use it when a site must recognize you as the same returning user on accounts you control. It stores cookies, localStorage, and the device fingerprint on a saved profile. Sessions connect over standard CDP and route through the profile’s proxy.









