Reuse sessions
Last updated Oct 12, 2026 By default, each Browser Sessions request launches a new browser instance. Reusing sessions eliminates cold-start time and improves performance by reconnecting to an existing browser instead of launching a new one.
This feature applies to Browser Sessions (Puppeteer, Playwright, and CDP). Quick Actions handle session lifecycle automatically.
There are two approaches to reusing sessions:
- Shared browser session (covered in this page): Connect multiple clients to a running browser. Create a separate browser context for each request to isolate cookies and storage.
- Durable Objects: Persist a browser for stateful session management. Use this approach to route users to specific sessions or coordinate conflicting operations.
1. Create a Worker project
Cloudflare Workers provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure. Your Worker application is a container to interact with a headless browser to do actions, such as taking screenshots.
Create a new Worker project named browser-worker by running:
npmyarnpnpm
npm create cloudflare@latest -- browser-worker
yarn create cloudflare browser-worker
pnpm create cloudflare@latest browser-worker
For setup, select the following options:
- For What would you like to start with?, choose
Hello World example. - For Which template would you like to use?, choose
Worker only. - For Which language do you want to use?, choose
TypeScript. - For Do you want to use git for version control?, choose
Yes. - For Do you want to add an AGENTS.md file to help AI coding tools understand Cloudflare APIs?, choose
Yes. - For Do you want to deploy your application?, choose
No(we will be making some changes before deploying).
2. Install Puppeteer
In your browser-worker directory, install Cloudflare's fork of Puppeteer:
npmyarnpnpmbun
npm i -D @cloudflare/puppeteer
yarn add -D @cloudflare/puppeteer
pnpm add -D @cloudflare/puppeteer
bun add -d @cloudflare/puppeteer
Note
Concurrent connections require @cloudflare/puppeteer version 1.1.0 or later. For Playwright, they require @cloudflare/playwright version 1.3.0 or later. Older versions use the legacy single-connection workflow.
3. Configure the Wrangler configuration file
Note
Your Worker configuration must include the nodejs_compat compatibility flag and a compatibility_date of 2025-09-15 or later.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "browser-worker",
"main": "src/index.ts",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-10-27",
"compatibility_flags": ["nodejs_compat"],
"browser": {
"binding": "MYBROWSER",
},
}
"$schema" = "./node_modules/wrangler/config-schema.json"
name = "browser-worker"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"
compatibility_flags = [ "nodejs_compat" ]
[browser]
binding = "MYBROWSER"
4. Code
The script lists active browser sessions and starts with one selected at random. Multiple clients can connect to the same session concurrently. Each puppeteer.connect() call creates an independent Chrome DevTools Protocol (CDP) connection.
Each request creates a browser context to isolate its pages, cookies, and storage. When the request finishes, the script closes the context. It then calls browser.disconnect() to close its CDP connection without terminating the shared browser.
A listed session might expire before the connection completes or have no remaining capacity. The script tries the other sessions before launching a new one.
If the browser receives no commands for longer than the idle limit, it closes automatically. Send enough requests to keep it alive.
import puppeteer from "@cloudflare/puppeteer";
const MAX_CONCURRENT_CONTEXTS = 4; // adjust according to average workload
export default {
async fetch(request, env) {
const url = new URL(request.url);
let reqUrl = url.searchParams.get("url") || "https://example.com";
reqUrl = new URL(reqUrl).toString(); // normalize
// Start with a random active session, then try the remaining sessions
const sessionIds = await this.getSessionIds(env.MYBROWSER);
let browser;
let launched = false;
for (const sessionId of sessionIds) {
try {
const candidate = await puppeteer.connect(env.MYBROWSER, sessionId);
try {
if (await this.hasCapacity(candidate)) {
browser = candidate;
break;
}
} finally {
if (candidate !== browser) {
await candidate.disconnect();
}
}
} catch (e) {
// The session may have closed after it was listed
console.log(`Failed to connect to ${sessionId}. Error ${e}`);
}
}
if (!browser) {
// No active session was available, so launch a new session
browser = await puppeteer.launch(env.MYBROWSER);
launched = true;
}
const sessionId = browser.sessionId();
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
const response = await page.goto(reqUrl);
const html = await response.text();
return new Response(
`${launched ? "Launched" : "Connected to"} ${sessionId} \n-----\n` +
html,
{
headers: {
"content-type": "text/plain",
},
},
);
} finally {
await context.close();
await browser.disconnect();
}
},
async hasCapacity(browser) {
const client = await browser.target().createCDPSession();
try {
const { browserContextIds } = await client.send(
"Target.getBrowserContexts",
);
return browserContextIds.length < MAX_CONCURRENT_CONTEXTS;
} finally {
await client.detach();
}
},
async getSessionIds(endpoint) {
const sessions = await puppeteer.sessions(endpoint);
console.log(`Sessions: ${JSON.stringify(sessions)}`);
if (sessions.length === 0) {
return [];
}
const startIndex = Math.floor(Math.random() * sessions.length);
return [
...sessions.slice(startIndex),
...sessions.slice(0, startIndex),
].map((session) => session.sessionId);
},
};
import puppeteer, {
type ActiveSession,
type Browser,
type BrowserWorker,
} from "@cloudflare/puppeteer";
const MAX_CONCURRENT_CONTEXTS = 4; // adjust according to average workload
interface Env {
MYBROWSER: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
let reqUrl = url.searchParams.get("url") || "https://example.com";
reqUrl = new URL(reqUrl).toString(); // normalize
// Start with a random active session, then try the remaining sessions
const sessionIds = await this.getSessionIds(env.MYBROWSER);
let browser;
let launched = false;
for (const sessionId of sessionIds) {
try {
const candidate = await puppeteer.connect(env.MYBROWSER, sessionId);
try {
if (await this.hasCapacity(candidate)) {
browser = candidate;
break;
}
} finally {
if (candidate !== browser) {
await candidate.disconnect();
}
}
} catch (e) {
// The session may have closed after it was listed
console.log(`Failed to connect to ${sessionId}. Error ${e}`);
}
}
if (!browser) {
// No active session was available, so launch a new session
browser = await puppeteer.launch(env.MYBROWSER);
launched = true;
}
const sessionId = browser.sessionId();
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
const response = await page.goto(reqUrl);
const html = await response!.text();
return new Response(
`${launched ? "Launched" : "Connected to"} ${sessionId} \n-----\n` +
html,
{
headers: {
"content-type": "text/plain",
},
},
);
} finally {
await context.close();
await browser.disconnect();
}
},
async hasCapacity(browser: Browser): Promise<boolean> {
const client = await browser.target().createCDPSession();
try {
const { browserContextIds } = await client.send(
"Target.getBrowserContexts",
);
return browserContextIds.length < MAX_CONCURRENT_CONTEXTS;
} finally {
await client.detach();
}
},
async getSessionIds(endpoint: BrowserWorker): Promise<string[]> {
const sessions: ActiveSession[] = await puppeteer.sessions(endpoint);
console.log(`Sessions: ${JSON.stringify(sessions)}`);
if (sessions.length === 0) {
return [];
}
const startIndex = Math.floor(Math.random() * sessions.length);
return [
...sessions.slice(startIndex),
...sessions.slice(0, startIndex),
].map((session) => session.sessionId);
},
};
Do not call browser.close() when other clients share the session. This method terminates the browser and disconnects every client. Browser contexts isolate request state, but they do not coordinate browser-wide operations.
Besides puppeteer.sessions(), Puppeteer provides other session management methods.
5. Test
Run npx wrangler dev to test your Worker locally.
Use real headless browser during local development
To interact with a real headless browser during local development, set "remote" : true in the Browser binding configuration. Learn more in our remote bindings documentation.
To test go to the following URL:
<LOCAL_HOST_URL>/?url=https://example.com
6. Deploy
Run npx wrangler deploy to deploy your Worker to the Cloudflare global network and then to go to the following URL:
<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/?url=https://example.com
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/browser-run/features/reuse-sessions/#page","headline":"Reuse sessions","description":"Improve Browser Run performance by reconnecting to existing browser sessions instead of launching new instances.","url":"https://developers.cloudflare.com/browser-run/features/reuse-sessions/","inLanguage":"en","image":"https://developers.cloudflare.com/browser-run/features/reuse-sessions/og.png?v=bfde937836ca8543","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Record and replay Browser Run sessions to visually debug browser automation scripts. title: Session recording image: https://developers.cloudflare.com/browser-run/features/session-recording/og.png?v=e0b4f537b14e70ba
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/browser-run/llms.txt
Use this file to discover all available pages before exploring further.
Docs