Running Playwright in Cloudflare Workers: Browser Automation at the Edge
Hook
Browser automation typically takes 2-3 seconds just to launch Chrome. Cloudflare’s Playwright fork eliminates that entirely, running automation in milliseconds by connecting to pre-warmed browsers at the edge.
Context
Traditional browser automation has always been infrastructure-heavy. When you use Playwright or Puppeteer, your code spawns a Chromium process, waits for it to initialize, then communicates over Chrome DevTools Protocol. This works fine for CI/CD pipelines or background jobs, but it’s a disaster for user-facing APIs. A simple “generate a screenshot” endpoint becomes a 3-second ordeal because you’re paying the browser launch tax on every request.
Cloudflare saw an opportunity: they already run a massive edge network with distributed compute (Workers) and recently launched Browser Rendering, a service that maintains pools of ready-to-go browser instances. But there was a problem. Playwright was built for Node.js—it expects filesystem access, child process spawning, and a full runtime. Workers run in V8 isolates with none of that. This fork bridges that gap, adapting Playwright’s API to work within Workers’ constraints while connecting to Cloudflare’s managed browser infrastructure instead of local instances.
Technical Insight
The architectural challenge here is fascinating: take a library designed for unrestricted Node.js environments and make it work in one of the most restricted JavaScript runtimes available. Workers don’t have a filesystem (beyond a small /tmp), can’t spawn processes, and limit CPU time. The fork accomplishes this through strategic surgery on Playwright’s client layer.
First, all the browser launching logic gets ripped out. In standard Playwright, you call chromium.launch() which spawns a local process. In the Cloudflare fork, you instead pass a browser binding from your Workers environment:
import playwright from '@cloudflare/playwright';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await playwright.chromium.connect(env.BROWSER);
const page = await browser.newPage();
await page.goto('https://example.com');
const screenshot = await page.screenshot();
await browser.close();
return new Response(screenshot, {
headers: { 'Content-Type': 'image/png' }
});
}
};
Notice the connect(env.BROWSER) call—that BROWSER binding is configured in your wrangler.toml and represents a connection to Cloudflare’s Browser Rendering service. Behind the scenes, this establishes a WebSocket connection to a browser instance running in Cloudflare’s infrastructure, potentially thousands of miles away from your code.
The fork maintains Playwright’s core API surface—page.goto(), page.screenshot(), selectors, and assertions all work as expected. This is critical for developer experience. If you’ve already written Playwright scripts, much of that knowledge transfers directly. The tracing API even works, writing trace files to the limited /tmp directory that Workers provides.
The connection model is where things get interesting from a performance perspective. Traditional browser automation has massive cold start overhead: launch process (1-2s), initialize browser internals (500ms-1s), establish DevTools connection (100-200ms). Cloudflare pre-launches browsers and keeps them warm. When your Worker requests a browser, it’s already running. You’re just establishing a WebSocket and starting a new browsing context. This typically completes in 50-100ms—a 20-30x improvement.
The tradeoff is latency on every subsequent command. When Playwright runs locally, DevTools Protocol messages travel over localhost. With this fork, every page.click() or page.evaluate() crosses the internet to wherever the browser instance lives. For automation with many small operations, this chattiness adds up. The sweet spot is workflows with few, heavyweight operations—like navigating to a page and taking a screenshot, or filling a form and generating a PDF.
Here’s a more realistic example showing PDF generation with custom viewport and authentication:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const targetUrl = url.searchParams.get('url');
if (!targetUrl) {
return new Response('Missing url parameter', { status: 400 });
}
const browser = await playwright.chromium.connect(env.BROWSER);
try {
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Mozilla/5.0 Custom Bot'
});
const page = await context.newPage();
// Add authentication header if needed
await page.setExtraHTTPHeaders({
'Authorization': `Bearer ${env.API_TOKEN}`
});
await page.goto(targetUrl, { waitUntil: 'networkidle' });
// Wait for dynamic content
await page.waitForSelector('.content-loaded', { timeout: 5000 });
const pdf = await page.pdf({
format: 'A4',
printBackground: true
});
await browser.close();
return new Response(pdf, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="page.pdf"'
}
});
} catch (error) {
await browser.close();
return new Response(`Error: ${error.message}`, { status: 500 });
}
}
};
One subtle but important detail: you must enable the nodejs_compat compatibility flag in your Workers configuration. The fork relies on certain Node.js standard library APIs (like buffer handling) that this flag provides. Without it, you’ll hit runtime errors when Playwright tries to process binary data from screenshots or PDFs.
The fork is based on Playwright 1.57.0, which means it lags behind the upstream project. Microsoft releases new Playwright versions every few weeks with bug fixes, new browser features, and API improvements. Cloudflare must periodically merge these changes and test them against the Workers environment, creating an inherent lag. For most use cases this doesn’t matter, but if you need a specific feature from Playwright 1.60+ or a critical security fix, you might be waiting.
Gotcha
The limitations hit hard if you’re coming from full Playwright. No Firefox or WebKit support means true cross-browser testing is impossible—you’re locked into Chromium. The Playwright Test runner, which many teams use as their primary testing framework, simply doesn’t work. You get the assertion library (@playwright/test/expect) but none of the test execution, fixtures, or reporting infrastructure. This makes the fork unsuitable as a drop-in replacement for existing Playwright Test suites.
Video recording is listed as unsupported, which makes sense given the distributed nature of the architecture. Component testing (Playwright’s feature for testing React/Vue components in isolation) doesn’t work either. The API surface is Playwright-shaped, but it’s a subset focused on basic automation primitives. If your automation relies on advanced features like mobile device emulation with specific Chrome versions, or HAR file recording for network analysis, check the documentation carefully—many features are partially implemented or missing entirely. The fork also doesn’t support connecting to your own browser instances; you must use Cloudflare’s Browser Rendering service, which comes with usage costs beyond your Workers charges.
Verdict
Use if: You need browser automation as part of user-facing APIs where cold start time matters, you’re building screenshot/PDF generation services that need global distribution and instant scaling, or you’re doing simple web scraping as part of edge compute workflows. The performance profile (sub-100ms cold starts, distributed execution) makes this ideal for on-demand rendering at scale. The Cloudflare Workers ecosystem (KV, D1, R2) integrates naturally, enabling powerful edge applications. Skip if: You need the Playwright Test framework for actual testing infrastructure, require Firefox or WebKit for cross-browser validation, depend on video recording or component testing, or need bleeding-edge Playwright features. For comprehensive E2E testing or complex automation pipelines with many sequential operations, traditional Playwright on dedicated infrastructure remains the better choice despite slower cold starts.