
Cloudflare Browser Rendering
- 43 installs
- 16 repo stars
- Updated November 20, 2025
- jackspace/claudeskillz
Automates headless Chrome on Cloudflare Workers with Puppeteer or Playwright for screenshots, PDF generation, web scraping, and browser session management.
About
A skill for Cloudflare Browser Rendering, running headless Chrome on Workers via Puppeteer and Playwright. Developers use it for screenshots, PDF generation, web scraping, and browser automation on the edge.
- @cloudflare/puppeteer and @cloudflare/playwright on Workers
- Screenshots, PDFs, scraping, and browser session reuse
Cloudflare Browser Rendering by the numbers
- 43 all-time installs (skills.sh)
- Ranked #744 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-browser-renderingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 43 |
|---|---|
| repo stars | ★ 16 |
| Last updated | November 20, 2025 |
| Repository | jackspace/claudeskillz ↗ |
What it does
Automates headless Chrome on Cloudflare Workers with Puppeteer or Playwright for screenshots, PDF generation, web scraping, and browser session management.
Files
Cloudflare Browser Rendering - Complete Reference
Production-ready knowledge domain for building browser automation workflows with Cloudflare Browser Rendering.
Status: Production Ready ✅ Last Updated: 2025-10-22 Dependencies: cloudflare-worker-base (for Worker setup) Latest Versions: @cloudflare/puppeteer@1.0.4, @cloudflare/playwright@1.0.0, wrangler@4.43.0
---
Table of Contents
1. Quick Start (5 minutes) 2. Browser Rendering Overview 3. Puppeteer API Reference 4. Playwright API Reference 5. Session Management 6. Common Patterns 7. Pricing & Limits 8. Known Issues Prevention 9. Production Checklist
---
Quick Start (5 minutes)
1. Add Browser Binding
wrangler.jsonc:
{
"name": "browser-worker",
"main": "src/index.ts",
"compatibility_date": "2023-03-14",
"compatibility_flags": ["nodejs_compat"],
"browser": {
"binding": "MYBROWSER"
}
}Why nodejs_compat? Browser Rendering requires Node.js APIs and polyfills.
2. Install Puppeteer
npm install @cloudflare/puppeteer3. Take Your First Screenshot
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { searchParams } = new URL(request.url);
const url = searchParams.get("url") || "https://example.com";
// Launch browser
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
// Navigate and capture
await page.goto(url);
const screenshot = await page.screenshot();
// Clean up
await browser.close();
return new Response(screenshot, {
headers: { "content-type": "image/png" }
});
}
};4. Deploy
npx wrangler deployTest at: https://your-worker.workers.dev/?url=https://example.com
CRITICAL:
- Always pass
env.MYBROWSERtopuppeteer.launch()(not undefined) - Always call
browser.close()when done (or usebrowser.disconnect()for session reuse) - Use
nodejs_compatcompatibility flag
---
Browser Rendering Overview
What is Browser Rendering?
Cloudflare Browser Rendering provides headless Chromium browsers running on Cloudflare's global network. Use familiar tools like Puppeteer and Playwright to automate browser tasks:
- Screenshots - Capture visual snapshots of web pages
- PDF Generation - Convert HTML/URLs to PDFs
- Web Scraping - Extract content from dynamic websites
- Testing - Automate frontend tests
- Crawling - Navigate multi-page workflows
Two Integration Methods
| Method | Best For | Complexity |
|---|---|---|
| Workers Bindings | Complex automation, custom workflows, session management | Advanced |
| REST API | Simple screenshot/PDF tasks | Simple |
This skill covers Workers Bindings (the advanced method with full Puppeteer/Playwright APIs).
Puppeteer vs Playwright
| Feature | Puppeteer | Playwright |
|---|---|---|
| API Familiarity | Most popular | Growing adoption |
| Package | @cloudflare/puppeteer@1.0.4 | @cloudflare/playwright@1.0.0 |
| Session Management | ✅ Advanced APIs | ⚠️ Basic |
| Browser Support | Chromium only | Chromium only (Firefox/Safari not yet supported) |
| Best For | Screenshots, PDFs, scraping | Testing, frontend automation |
Recommendation: Use Puppeteer for most use cases. Playwright is ideal if you're already using it for testing.
---
Puppeteer API Reference
puppeteer.launch()
Launch a new browser instance.
Signature:
await puppeteer.launch(binding: Fetcher, options?: LaunchOptions): Promise<Browser>Parameters:
binding(required) - Browser binding fromenv.MYBROWSERoptions(optional):keep_alive(number) - Keep browser alive for N milliseconds (max: 600000 = 10 minutes)
Returns: Promise<Browser> - Browser instance
Example:
const browser = await puppeteer.launch(env.MYBROWSER, {
keep_alive: 60000 // Keep alive for 60 seconds
});CRITICAL: Must pass env.MYBROWSER binding. Error "Cannot read properties of undefined (reading 'fetch')" means the binding wasn't passed.
---
puppeteer.connect()
Connect to an existing browser session.
Signature:
await puppeteer.connect(binding: Fetcher, sessionId: string): Promise<Browser>Use Cases:
- Reuse existing browser sessions for performance
- Share browser instance across multiple Workers
- Reduce startup time
Example:
const sessionId = "478f4d7d-e943-40f6-a414-837d3736a1dc";
const browser = await puppeteer.connect(env.MYBROWSER, sessionId);---
puppeteer.sessions()
List currently running browser sessions.
Signature:
await puppeteer.sessions(binding: Fetcher): Promise<SessionInfo[]>Returns:
interface SessionInfo {
sessionId: string;
startTime: number;
connectionId?: string; // Present if worker is connected
connectionStartTime?: number;
}Example:
const sessions = await puppeteer.sessions(env.MYBROWSER);
// Find sessions without active connections
const freeSessions = sessions.filter(s => !s.connectionId);---
puppeteer.history()
List recent sessions (both open and closed).
Signature:
await puppeteer.history(binding: Fetcher): Promise<HistoryEntry[]>Returns:
interface HistoryEntry {
sessionId: string;
startTime: number;
endTime?: number;
closeReason?: number;
closeReasonText?: string; // "NormalClosure", "BrowserIdle", etc.
}Use Case: Monitor usage patterns and debug session issues.
---
puppeteer.limits()
Check current account limits and available sessions.
Signature:
await puppeteer.limits(binding: Fetcher): Promise<LimitsInfo>Returns:
interface LimitsInfo {
activeSessions: Array<{ id: string }>;
maxConcurrentSessions: number;
allowedBrowserAcquisitions: number;
timeUntilNextAllowedBrowserAcquisition: number; // milliseconds
}Example:
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
return new Response("Rate limit reached", { status: 429 });
}---
Browser API
Methods available on the Browser object returned by launch() or connect().
browser.newPage()
Create a new page (tab) in the browser.
Signature:
await browser.newPage(): Promise<Page>Example:
const page = await browser.newPage();
await page.goto("https://example.com");Performance Tip: Reuse browser instances and open multiple tabs instead of launching new browsers.
---
browser.sessionId()
Get the current browser session ID.
Returns: string - Session ID
Example:
const sessionId = browser.sessionId();
console.log("Current session:", sessionId);---
browser.close()
Close the browser and terminate the session.
Signature:
await browser.close(): Promise<void>When to use: When you're completely done with the browser and want to free resources.
---
browser.disconnect()
Disconnect from the browser WITHOUT closing it.
Signature:
await browser.disconnect(): Promise<void>When to use: Session reuse - allows another Worker to connect to the same session later.
Example:
// Keep session alive for reuse
const sessionId = browser.sessionId();
await browser.disconnect(); // Don't close, just disconnect
// Later: puppeteer.connect(env.MYBROWSER, sessionId)---
browser.createBrowserContext()
Create an isolated incognito browser context.
Signature:
await browser.createBrowserContext(): Promise<BrowserContext>Use Cases:
- Isolate cookies and cache between operations
- Test multi-user scenarios
- Maintain session isolation while reusing browser
Example:
const context1 = await browser.createBrowserContext();
const context2 = await browser.createBrowserContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// page1 and page2 have separate cookies/cache---
Page API
Methods available on the Page object returned by browser.newPage().
page.goto()
Navigate to a URL.
Signature:
await page.goto(url: string, options?: NavigationOptions): Promise<Response>Options:
waitUntil- When to consider navigation complete:"load"- Wait for load event (default)"domcontentloaded"- Wait for DOMContentLoaded"networkidle0"- Wait until no network connections for 500ms"networkidle2"- Wait until ≤2 network connections for 500mstimeout- Maximum navigation time in milliseconds (default: 30000)
Example:
await page.goto("https://example.com", {
waitUntil: "networkidle0",
timeout: 60000
});Best Practice: Use "networkidle0" for dynamic content, "load" for static pages.
---
page.screenshot()
Capture a screenshot of the page.
Signature:
await page.screenshot(options?: ScreenshotOptions): Promise<Buffer>Options:
fullPage(boolean) - Capture full scrollable page (default: false)type(string) -"png"or"jpeg"(default:"png")quality(number) - JPEG quality 0-100 (only for jpeg)clip(object) - Capture specific region:{ x, y, width, height }
Examples:
// Full page screenshot
const screenshot = await page.screenshot({ fullPage: true });
// JPEG with compression
const screenshot = await page.screenshot({
type: "jpeg",
quality: 80
});
// Specific region
const screenshot = await page.screenshot({
clip: { x: 0, y: 0, width: 800, height: 600 }
});---
page.pdf()
Generate a PDF of the page.
Signature:
await page.pdf(options?: PDFOptions): Promise<Buffer>Options:
format(string) - Page format:"Letter","A4", etc.printBackground(boolean) - Include background graphics (default: false)margin(object) -{ top, right, bottom, left }(e.g.,"1cm")landscape(boolean) - Landscape orientation (default: false)scale(number) - Scale factor 0.1-2 (default: 1)
Example:
const pdf = await page.pdf({
format: "A4",
printBackground: true,
margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" }
});
return new Response(pdf, {
headers: { "content-type": "application/pdf" }
});---
page.content()
Get the full HTML content of the page.
Signature:
await page.content(): Promise<string>Example:
const html = await page.content();
console.log(html); // Full HTML source---
page.setContent()
Set custom HTML content.
Signature:
await page.setContent(html: string, options?: NavigationOptions): Promise<void>Use Case: Generate PDFs from custom HTML.
Example:
await page.setContent(`
<!DOCTYPE html>
<html>
<head><style>body { font-family: Arial; }</style></head>
<body><h1>Hello World</h1></body>
</html>
`);
const pdf = await page.pdf({ format: "A4" });---
page.evaluate()
Execute JavaScript in the browser context.
Signature:
await page.evaluate<T>(fn: () => T): Promise<T>Use Cases:
- Extract data from the DOM
- Manipulate page content
- Workaround for XPath (not directly supported)
Examples:
// Extract text content
const title = await page.evaluate(() => document.title);
// Extract structured data
const data = await page.evaluate(() => ({
title: document.title,
url: window.location.href,
headings: Array.from(document.querySelectorAll("h1, h2")).map(el => el.textContent),
links: Array.from(document.querySelectorAll("a")).map(el => el.href)
}));
// XPath workaround (XPath selectors not directly supported)
const innerHtml = await page.evaluate(() => {
return new XPathEvaluator()
.createExpression("/html/body/div/h1")
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
.singleNodeValue.innerHTML;
});---
page.waitForSelector()
Wait for an element to appear in the DOM.
Signature:
await page.waitForSelector(selector: string, options?: WaitForOptions): Promise<ElementHandle>Options:
timeout(number) - Maximum wait time in millisecondsvisible(boolean) - Wait for element to be visible
Example:
await page.goto("https://example.com");
await page.waitForSelector("#content", { visible: true });
const screenshot = await page.screenshot();---
page.type()
Type text into an input field.
Signature:
await page.type(selector: string, text: string): Promise<void>Example:
await page.type('input[name="email"]', 'user@example.com');---
page.click()
Click an element.
Signature:
await page.click(selector: string): Promise<void>Example:
await page.click('button[type="submit"]');
await page.waitForNavigation();---
Playwright API Reference
Playwright provides a similar API to Puppeteer with slight differences.
Installation
npm install @cloudflare/playwrightBasic Example
import { env } from "cloudflare:test";
import { chromium } from "@cloudflare/playwright";
interface Env {
BROWSER: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await chromium.launch(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" }
});
}
};Key Differences from Puppeteer
| Feature | Puppeteer | Playwright |
|---|---|---|
| Import | import puppeteer from "@cloudflare/puppeteer" | import { chromium } from "@cloudflare/playwright" |
| Launch | puppeteer.launch(env.MYBROWSER) | chromium.launch(env.BROWSER) |
| Session API | ✅ Advanced (sessions, history, limits) | ⚠️ Basic |
| Auto-waiting | Manual waitForSelector() | Built-in auto-waiting |
| Selectors | CSS only | CSS, text, XPath (via evaluate workaround) |
Recommendation: Stick with Puppeteer unless you have existing Playwright tests to migrate.
---
Session Management
Why Session Management Matters
Problem: Launching new browsers is slow and consumes concurrency limits.
Solution: Reuse browser sessions across requests.
Benefits:
- ⚡ Faster (no cold start)
- 💰 Lower concurrency usage
- 📊 Better resource utilization
---
Session Reuse Pattern
import puppeteer from "@cloudflare/puppeteer";
async function getBrowser(env: Env): Promise<{ browser: Browser; launched: boolean }> {
// Check for available sessions
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
if (freeSessions.length > 0) {
// Reuse existing session
try {
const browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
return { browser, launched: false };
} catch (e) {
console.log("Failed to connect, launching new browser");
}
}
// Launch new session
const browser = await puppeteer.launch(env.MYBROWSER);
return { browser, launched: true };
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { browser, launched } = await getBrowser(env);
try {
const page = await browser.newPage();
await page.goto("https://example.com");
const screenshot = await page.screenshot();
// Disconnect (don't close) to allow reuse
await browser.disconnect();
return new Response(screenshot, {
headers: { "content-type": "image/png" }
});
} catch (error) {
await browser.close(); // Close on error
throw error;
}
}
};CRITICAL:
- Use
browser.disconnect()to keep session alive - Use
browser.close()on errors - Always handle connection failures
---
Incognito Browser Contexts
Use browser contexts to isolate cookies and cache while sharing a browser instance.
Benefits:
- Share browser (reduce concurrency)
- Isolate sessions (separate cookies/cache)
- Test multi-user scenarios
Example:
const browser = await puppeteer.launch(env.MYBROWSER);
// Create isolated contexts
const context1 = await browser.createBrowserContext();
const context2 = await browser.createBrowserContext();
// Each context has its own cookies/cache
const page1 = await context1.newPage();
const page2 = await context2.newPage();
await page1.goto("https://app.example.com"); // User 1
await page2.goto("https://app.example.com"); // User 2
await context1.close();
await context2.close();
await browser.close();---
Multiple Tabs vs Multiple Browsers
Scenario: Scrape 10 URLs
❌ Bad (10 browsers):
for (const url of urls) {
const browser = await puppeteer.launch(env.MYBROWSER); // 10 launches!
// ... scrape ...
await browser.close();
}✅ Good (1 browser, 10 tabs):
const browser = await puppeteer.launch(env.MYBROWSER);
const results = await Promise.all(
urls.map(async (url) => {
const page = await browser.newPage();
await page.goto(url);
const data = await page.evaluate(() => ({
title: document.title,
text: document.body.innerText
}));
await page.close();
return { url, data };
})
);
await browser.close();Benefit: Uses 1 concurrent browser instead of 10.
---
Common Patterns
Pattern 1: Screenshot with KV Caching
Cache screenshots to reduce browser usage and improve performance.
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
CACHE: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) {
return new Response("Missing ?url parameter", { status: 400 });
}
const normalizedUrl = new URL(url).toString();
// Check cache
let screenshot = await env.CACHE.get(normalizedUrl, { type: "arrayBuffer" });
if (!screenshot) {
// Generate screenshot
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(normalizedUrl);
screenshot = await page.screenshot();
await browser.close();
// Cache for 24 hours
await env.CACHE.put(normalizedUrl, screenshot, {
expirationTtl: 60 * 60 * 24
});
}
return new Response(screenshot, {
headers: { "content-type": "image/png" }
});
}
};---
Pattern 2: PDF Generation from HTML
Convert custom HTML to PDF.
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const { html } = await request.json<{ html: string }>();
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
// Set custom HTML
await page.setContent(html, { waitUntil: "networkidle0" });
// Generate PDF
const pdf = await page.pdf({
format: "A4",
printBackground: true,
margin: {
top: "1cm",
right: "1cm",
bottom: "1cm",
left: "1cm"
}
});
await browser.close();
return new Response(pdf, {
headers: {
"content-type": "application/pdf",
"content-disposition": "attachment; filename=document.pdf"
}
});
}
};---
Pattern 3: Web Scraping with Structured Data
Extract structured data from web pages.
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
}
interface ProductData {
title: string;
price: string;
description: string;
image: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(url!, { waitUntil: "networkidle0" });
// Extract structured data
const data = await page.evaluate<ProductData>(() => {
return {
title: document.querySelector("h1")?.textContent || "",
price: document.querySelector(".price")?.textContent || "",
description: document.querySelector(".description")?.textContent || "",
image: document.querySelector("img")?.src || ""
};
});
await browser.close();
return Response.json({ url, data });
}
};---
Pattern 4: Batch Scraping Multiple URLs
Efficiently scrape multiple URLs using tabs.
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
}
async function scrapeUrl(browser: Browser, url: string): Promise<any> {
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: "networkidle0", timeout: 30000 });
const data = await page.evaluate(() => ({
title: document.title,
url: window.location.href,
text: document.body.innerText.slice(0, 500) // First 500 chars
}));
await page.close();
return { success: true, url, data };
} catch (error) {
await page.close();
return {
success: false,
url,
error: error instanceof Error ? error.message : "Unknown error"
};
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { urls } = await request.json<{ urls: string[] }>();
if (!urls || urls.length === 0) {
return new Response("Missing urls array", { status: 400 });
}
const browser = await puppeteer.launch(env.MYBROWSER);
// Scrape all URLs in parallel (each in its own tab)
const results = await Promise.all(
urls.map(url => scrapeUrl(browser, url))
);
await browser.close();
return Response.json({ results });
}
};---
Pattern 5: AI-Enhanced Scraping
Combine Browser Rendering with Workers AI to extract structured data.
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
AI: Ai;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
// Scrape page content
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(url!, { waitUntil: "networkidle0" });
const bodyContent = await page.$eval("body", el => el.innerHTML);
await browser.close();
// Extract structured data with AI
const response = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
messages: [
{
role: "user",
content: `Extract product information as JSON from this HTML. Include: name, price, description, availability.\n\nHTML:\n${bodyContent.slice(0, 4000)}`
}
]
});
// Parse AI response
let productData;
try {
productData = JSON.parse(response.response);
} catch {
productData = { raw: response.response };
}
return Response.json({ url, product: productData });
}
};---
Pattern 6: Form Filling and Automation
Automate form submissions and multi-step workflows.
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { email, password } = await request.json<{
email: string;
password: string;
}>();
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
// Navigate to login page
await page.goto("https://example.com/login");
// Fill form
await page.type('input[name="email"]', email);
await page.type('input[name="password"]', password);
// Submit and wait for navigation
await page.click('button[type="submit"]');
await page.waitForNavigation();
// Extract result
const result = await page.evaluate(() => ({
url: window.location.href,
title: document.title,
loggedIn: document.querySelector(".user-profile") !== null
}));
await browser.close();
return Response.json(result);
}
};---
Pricing & Limits
Free Tier (Workers Free)
| Feature | Limit |
|---|---|
| Browser Duration | 10 minutes per day |
| Concurrent Browsers | 3 per account |
| New Browsers per Minute | 3 per minute |
| REST API Requests | 6 per minute |
| Browser Timeout | 60 seconds (idle) |
Paid Tier (Workers Paid)
| Feature | Included | Beyond Included |
|---|---|---|
| Browser Duration | 10 hours per month | $0.09 per additional browser hour |
| Concurrent Browsers | 10 (monthly average) | $2.00 per additional concurrent browser |
| New Browsers per Minute | 30 per minute | Request higher limit |
| REST API Requests | 180 per minute | Request higher limit |
| Browser Timeout | 60 seconds (can extend to 10 minutes with keep_alive) | - |
| Max Concurrent Browsers | 30 per account | Request higher limit |
Pricing Calculation
Duration Charges:
- Charged per browser hour
- Rounded to nearest hour at end of billing cycle
- Failed requests (timeouts) are NOT charged
Concurrency Charges:
- Monthly average of daily peak usage
- Example: 10 browsers for 15 days, 20 browsers for 15 days = (10×15 + 20×15) / 30 = 15 average
- 15 average - 10 included = 5 × $2.00 = $10.00
Example Monthly Bill:
- 50 browser hours used: (50 - 10) × $0.09 = $3.60
- 15 concurrent browsers average: (15 - 10) × $2.00 = $10.00
- Total: $13.60
Rate Limiting
Per-Second Rate: Rate limits are enforced per-second. Example:
- 180 requests per minute = 3 requests per second
- You cannot send all 180 at once; they must be spread evenly
Handling Rate Limits:
async function launchWithRetry(env: Env, maxRetries = 3): Promise<Browser> {
for (let i = 0; i < maxRetries; i++) {
try {
return await puppeteer.launch(env.MYBROWSER);
} catch (error) {
if (i === maxRetries - 1) throw error;
// Check if rate limited
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
// Wait before retry
const delay = limits.timeUntilNextAllowedBrowserAcquisition || 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error("Failed to launch browser");
}---
Known Issues Prevention
This skill prevents 6 documented issues:
---
Issue #1: XPath Selectors Not Supported
Error: "XPath selector not supported" or selector failures Source: https://developers.cloudflare.com/browser-rendering/faq/#why-cant-i-use-an-xpath-selector-when-using-browser-rendering-with-puppeteer Why It Happens: XPath poses a security risk to Workers Prevention: Use CSS selectors or page.evaluate() with XPathEvaluator
Solution:
// ❌ Don't use XPath directly (not supported)
// await page.$x('/html/body/div/h1')
// ✅ Use CSS selector
const heading = await page.$("div > h1");
// ✅ Or use XPath in page.evaluate()
const innerHtml = await page.evaluate(() => {
return new XPathEvaluator()
.createExpression("/html/body/div/h1")
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
.singleNodeValue.innerHTML;
});---
Issue #2: Browser Binding Not Passed
Error: "Cannot read properties of undefined (reading 'fetch')" Source: https://developers.cloudflare.com/browser-rendering/faq/#cannot-read-properties-of-undefined-reading-fetch Why It Happens: puppeteer.launch() called without browser binding Prevention: Always pass env.MYBROWSER to launch
Solution:
// ❌ Missing browser binding
const browser = await puppeteer.launch(); // Error!
// ✅ Pass binding
const browser = await puppeteer.launch(env.MYBROWSER);---
Issue #3: Browser Timeout (60 seconds)
Error: Browser closes unexpectedly after 60 seconds Source: https://developers.cloudflare.com/browser-rendering/platform/limits/#note-on-browser-timeout Why It Happens: Default timeout is 60 seconds of inactivity Prevention: Use keep_alive option to extend up to 10 minutes
Solution:
// Extend timeout to 5 minutes for long-running tasks
const browser = await puppeteer.launch(env.MYBROWSER, {
keep_alive: 300000 // 5 minutes = 300,000 ms
});Note: Browser closes if no devtools commands for the specified duration.
---
Issue #4: Concurrency Limits Reached
Error: "Rate limit exceeded" or new browser launch fails Source: https://developers.cloudflare.com/browser-rendering/platform/limits/ Why It Happens: Exceeded concurrent browser limit (3 free, 10-30 paid) Prevention: Reuse sessions, use tabs instead of multiple browsers, check limits before launching
Solutions:
// 1. Check limits before launching
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
return new Response("Concurrency limit reached", { status: 429 });
}
// 2. Reuse sessions
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
if (freeSessions.length > 0) {
const browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
}
// 3. Use tabs instead of multiple browsers
const browser = await puppeteer.launch(env.MYBROWSER);
const page1 = await browser.newPage();
const page2 = await browser.newPage(); // Same browser, different tabs---
Issue #5: Local Development Request Size Limit
Error: Request larger than 1MB fails in wrangler dev Source: https://developers.cloudflare.com/browser-rendering/faq/#does-local-development-support-all-browser-rendering-features Why It Happens: Local development limitation Prevention: Use remote: true in browser binding for local dev
Solution:
// wrangler.jsonc for local development
{
"browser": {
"binding": "MYBROWSER",
"remote": true // Use real headless browser during dev
}
}---
Issue #6: Bot Protection Always Triggered
Error: Website blocks requests as bot traffic Source: https://developers.cloudflare.com/browser-rendering/faq/#will-browser-rendering-bypass-cloudflares-bot-protection Why It Happens: Browser Rendering requests always identified as bots Prevention: Cannot bypass; if scraping your own zone, create WAF skip rule
Solution:
// ❌ Cannot bypass bot protection
// Requests will always be identified as bots
// ✅ If scraping your own Cloudflare zone:
// 1. Go to Security > WAF > Custom rules
// 2. Create skip rule with custom header:
// Header: X-Custom-Auth
// Value: your-secret-token
// 3. Pass header in your scraping requests
// Note: Automatic headers are included:
// - cf-biso-request-id
// - cf-biso-devtools---
Production Checklist
Before deploying Browser Rendering Workers to production:
Configuration
- [ ] Browser binding configured in wrangler.jsonc
- [ ] nodejs_compat flag enabled (required for Browser Rendering)
- [ ] Keep-alive timeout set if tasks take > 60 seconds
- [ ] Remote binding enabled for local development if needed
Error Handling
- [ ] Retry logic implemented for rate limits
- [ ] Timeout handling for page.goto()
- [ ] Browser cleanup in try-finally blocks
- [ ] Concurrency limit checks before launching browsers
- [ ] Graceful degradation when browser unavailable
Performance
- [ ] Session reuse implemented for high-traffic routes
- [ ] Multiple tabs used instead of multiple browsers
- [ ] Incognito contexts for session isolation
- [ ] KV caching for repeated screenshots/PDFs
- [ ] Batch operations to maximize browser utilization
Monitoring
- [ ] Log browser session IDs for debugging
- [ ] Track browser duration for billing estimates
- [ ] Monitor concurrency usage with puppeteer.limits()
- [ ] Alert on rate limit errors
- [ ] Dashboard monitoring at https://dash.cloudflare.com/?to=/:account/workers/browser-rendering
Security
- [ ] Input validation for URLs (prevent SSRF)
- [ ] Timeout limits to prevent abuse
- [ ] Rate limiting on public endpoints
- [ ] Authentication for sensitive scraping endpoints
- [ ] WAF rules if scraping your own zone
Testing
- [ ] Test screenshot capture with various page sizes
- [ ] Test PDF generation with custom HTML
- [ ] Test scraping with dynamic content (networkidle0)
- [ ] Test error scenarios (invalid URLs, timeouts)
- [ ] Load test concurrency limits
---
Error Handling Template
Complete error handling for production use:
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
}
async function withBrowser<T>(
env: Env,
fn: (browser: Browser) => Promise<T>
): Promise<T> {
let browser: Browser | null = null;
try {
// Check limits
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
throw new Error("Rate limit reached. Retry after: " + limits.timeUntilNextAllowedBrowserAcquisition + "ms");
}
// Launch or connect
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
if (freeSessions.length > 0) {
try {
browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
} catch {
browser = await puppeteer.launch(env.MYBROWSER);
}
} else {
browser = await puppeteer.launch(env.MYBROWSER);
}
// Execute user function
const result = await fn(browser);
// Disconnect (keep session alive)
await browser.disconnect();
return result;
} catch (error) {
// Close on error
if (browser) {
await browser.close();
}
throw error;
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
const screenshot = await withBrowser(env, async (browser) => {
const page = await browser.newPage();
await page.goto("https://example.com", {
waitUntil: "networkidle0",
timeout: 30000
});
return await page.screenshot();
});
return new Response(screenshot, {
headers: { "content-type": "image/png" }
});
} catch (error) {
console.error("Browser error:", error);
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "Unknown error"
}),
{ status: 500, headers: { "content-type": "application/json" } }
);
}
}
};---
Using Bundled Resources
Templates (templates/)
Ready-to-use code templates for common patterns:
basic-screenshot.ts- Minimal screenshot examplescreenshot-with-kv-cache.ts- Screenshot with KV cachingpdf-generation.ts- Generate PDFs from HTML or URLsweb-scraper-basic.ts- Basic web scraping patternweb-scraper-batch.ts- Batch scrape multiple URLssession-reuse.ts- Session reuse for performanceai-enhanced-scraper.ts- Scraping with Workers AIplaywright-example.ts- Playwright alternative examplewrangler-browser-config.jsonc- Browser binding configuration
Usage:
# Copy template to your project
cp ~/.claude/skills/cloudflare-browser-rendering/templates/basic-screenshot.ts src/index.tsReferences (references/)
Deep-dive documentation:
session-management.md- Complete session reuse guidepricing-and-limits.md- Detailed pricing breakdowncommon-errors.md- All known issues and solutionspuppeteer-vs-playwright.md- Feature comparison and migration
When to load: Reference when implementing advanced patterns or debugging specific issues.
---
Dependencies
Required:
@cloudflare/puppeteer@1.0.4- Puppeteer for Workerswrangler@4.43.0+- Cloudflare CLI
Optional:
@cloudflare/playwright@1.0.0- Playwright for Workers (alternative)@cloudflare/workers-types@4.20251014.0+- TypeScript types
Related Skills:
cloudflare-worker-base- Worker setup with Honocloudflare-kv- KV caching for screenshotscloudflare-r2- R2 storage for generated filescloudflare-workers-ai- AI-enhanced scraping
---
Official Documentation
- Browser Rendering Docs: https://developers.cloudflare.com/browser-rendering/
- Puppeteer API: https://pptr.dev/api/
- Playwright API: https://playwright.dev/docs/api/class-playwright
- Cloudflare Puppeteer Fork: https://github.com/cloudflare/puppeteer
- Cloudflare Playwright Fork: https://github.com/cloudflare/playwright
- Pricing: https://developers.cloudflare.com/browser-rendering/platform/pricing/
- Limits: https://developers.cloudflare.com/browser-rendering/platform/limits/
---
Package Versions (Verified 2025-10-22)
{
"dependencies": {
"@cloudflare/puppeteer": "^1.0.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20251014.0",
"wrangler": "^4.43.0"
}
}Alternative (Playwright):
{
"dependencies": {
"@cloudflare/playwright": "^1.0.0"
}
}---
Troubleshooting
Problem: "Cannot read properties of undefined (reading 'fetch')"
Solution: Pass browser binding to puppeteer.launch():
const browser = await puppeteer.launch(env.MYBROWSER); // Not just puppeteer.launch()Problem: XPath selectors not working
Solution: Use CSS selectors or page.evaluate() with XPathEvaluator (see Issue #1)
Problem: Browser closes after 60 seconds
Solution: Extend timeout with keep_alive:
const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 300000 });Problem: Rate limit reached
Solution: Reuse sessions, use tabs, check limits before launching (see Issue #4)
Problem: Local dev request > 1MB fails
Solution: Enable remote binding in wrangler.jsonc:
{ "browser": { "binding": "MYBROWSER", "remote": true } }Problem: Website blocks as bot
Solution: Cannot bypass. If your own zone, create WAF skip rule (see Issue #6)
---
Questions? Issues?
1. Check references/common-errors.md for detailed solutions 2. Review references/session-management.md for performance optimization 3. Verify browser binding is configured in wrangler.jsonc 4. Check official docs: https://developers.cloudflare.com/browser-rendering/ 5. Ensure nodejs_compat compatibility flag is enabled
Cloudflare Browser Rendering Skill
Auto-Discovery Skill for Claude Code CLI
Complete knowledge domain for Cloudflare Browser Rendering - Headless Chrome automation with Puppeteer and Playwright on Cloudflare Workers.
---
Auto-Trigger Keywords
Claude will automatically suggest this skill when you mention any of these keywords:
Primary Triggers (Technologies)
- browser rendering
- cloudflare browser rendering
- @cloudflare/puppeteer
- @cloudflare/playwright
- puppeteer workers
- playwright workers
- headless chrome workers
- headless browser cloudflare
- browser automation cloudflare
- browser binding
- puppeteer.launch
- chromium.launch
Secondary Triggers (Use Cases)
- screenshot cloudflare
- screenshot workers
- take screenshot puppeteer
- pdf generation workers
- generate pdf puppeteer
- web scraping cloudflare
- scrape website workers
- crawl website puppeteer
- browser automation
- headless testing
- puppeteer screenshot
- playwright screenshot
Commands & API Triggers
- puppeteer.launch
- puppeteer.connect
- puppeteer.sessions
- puppeteer.history
- puppeteer.limits
- browser.newPage
- browser.sessionId
- browser.close
- browser.disconnect
- browser.createBrowserContext
- page.goto
- page.screenshot
- page.pdf
- page.evaluate
- page.waitForSelector
- page.type
- page.click
- incognito context
- browser tabs
- session reuse
Configuration Triggers
- wrangler browser binding
- nodejs_compat browser
- browser binding config
- remote browser binding
- keep_alive option
Error-Based Triggers
- "Cannot read properties of undefined (reading 'fetch')"
- "XPath selector not supported"
- "XPath not supported"
- browser timeout
- browser closed unexpectedly
- concurrency limit reached
- rate limit browser
- too many browsers
- browser memory limit
- "statement too long" puppeteer
- "Network connection lost"
- bot protection triggered
- request larger than 1MB
---
What This Skill Does
- ✅ Configures browser bindings in wrangler.jsonc with nodejs_compat
- ✅ Launches Puppeteer/Playwright browsers on Workers
- ✅ Captures screenshots (full page, regions, PNG/JPEG)
- ✅ Generates PDFs from HTML or URLs with custom formatting
- ✅ Scrapes web content with structured data extraction
- ✅ Manages browser sessions for performance optimization
- ✅ Implements session reuse to reduce concurrency usage
- ✅ Creates incognito contexts for session isolation
- ✅ Handles batch operations with multiple tabs
- ✅ Integrates with Workers AI for AI-enhanced scraping
- ✅ Provides error handling and retry logic patterns
---
Known Issues Prevented
| Issue | Error Message | How Skill Prevents |
|---|---|---|
| XPath Not Supported | "XPath selector not supported" | Templates use CSS selectors or page.evaluate() workaround |
| Missing Browser Binding | "Cannot read properties of undefined (reading 'fetch')" | Always passes env.MYBROWSER to puppeteer.launch() |
| Browser Timeout | Browser closes after 60s | Documents keep_alive option to extend up to 10 minutes |
| Concurrency Limits | "Rate limit reached" | Session reuse patterns, limit checks before launching |
| Local Dev Limit | Requests > 1MB fail in dev | Documents remote: true for local development |
| Bot Protection | Website blocks requests | Explains bot identification, WAF skip rule workaround |
---
Token Efficiency
Manual Browser Rendering Setup (Without Skill):
- Configure browser binding: 600 tokens
- Learn Puppeteer API: 2,000 tokens
- Implement screenshots: 1,200 tokens
- Implement scraping: 1,500 tokens
- Add session management: 2,000 tokens
- Handle errors: 1,000 tokens
- Debug XPath issues: 800 tokens
- Total: ~9,100 tokens
With cloudflare-browser-rendering Skill:
- Reference skill templates: 2,000 tokens
- Customize for your use case: 1,500 tokens
- Total: ~3,500 tokens
Savings: ~62% token reduction (5,600 tokens saved)
---
When to Use This Skill
✅ Use When:
- Taking screenshots of websites or web applications
- Generating PDFs from HTML content or URLs
- Scraping dynamic websites that require JavaScript execution
- Crawling multi-page websites
- Automating browser workflows (form filling, navigation)
- Testing web applications with headless browsers
- Extracting structured data from web pages
- Monitoring website changes or visual regression testing
- Need session management for performance optimization
- Building screenshot-as-a-service APIs
❌ Don't Use When:
- Static HTML parsing → Use simple fetch() + HTML parser
- Need to bypass bot protection → Cannot bypass (always identified as bot)
- Simple HTTP requests → Use fetch() API
- Real-time browser control → Workers run on edge, not suitable for interactive use
- Need Firefox/Safari → Only Chromium supported
- Large-scale crawling → Consider rate limits and costs
---
Quick Usage Example
# Configure wrangler.jsonc
{
"browser": { "binding": "MYBROWSER" },
"compatibility_flags": ["nodejs_compat"]
}
# Install
npm install @cloudflare/puppeteerimport puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await puppeteer.launch(env.MYBROWSER);
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" }
});
}
};---
File Structure
~/.claude/skills/cloudflare-browser-rendering/
├── SKILL.md # Complete reference
├── README.md # This file (auto-trigger keywords)
├── templates/
│ ├── basic-screenshot.ts # Minimal screenshot example
│ ├── screenshot-with-kv-cache.ts # Screenshot + KV caching
│ ├── pdf-generation.ts # PDF generation from HTML/URL
│ ├── web-scraper-basic.ts # Basic web scraping
│ ├── web-scraper-batch.ts # Batch scraping multiple URLs
│ ├── session-reuse.ts # Session reuse pattern
│ ├── ai-enhanced-scraper.ts # Scraping + Workers AI
│ ├── playwright-example.ts # Playwright alternative
│ └── wrangler-browser-config.jsonc # Browser binding config
├── references/
│ ├── session-management.md # Session optimization guide
│ ├── pricing-and-limits.md # Complete pricing breakdown
│ ├── common-errors.md # All known issues + solutions
│ └── puppeteer-vs-playwright.md # Feature comparison
└── scripts/
└── check-versions.sh # Verify package versions---
Dependencies
- Required: cloudflare-worker-base skill (for Worker setup)
- CLI: wrangler@4.43.0+
- Package: @cloudflare/puppeteer@1.0.4 or @cloudflare/playwright@1.0.0
- Types: @cloudflare/workers-types@4.20251014.0+
---
Related Skills
cloudflare-worker-base- Base Worker + Hono setupcloudflare-kv- KV caching for screenshotscloudflare-r2- R2 storage for generated filescloudflare-workers-ai- AI-enhanced scrapingcloudflare-agents- Agent SDK with browser rendering
---
Common Patterns Included
1. Basic Screenshot - Simple screenshot capture 2. Screenshot with KV Caching - Production pattern with caching 3. PDF Generation - Convert HTML to PDF with custom formatting 4. Web Scraping - Extract structured data from web pages 5. Batch Scraping - Scrape multiple URLs efficiently with tabs 6. Session Reuse - Optimize performance by reusing browser sessions 7. AI-Enhanced Scraping - Combine browser rendering with Workers AI 8. Playwright Alternative - Playwright implementation examples
---
Pricing Overview
Free Tier:
- 10 minutes browser time per day
- 3 concurrent browsers
- 60 second browser timeout
Paid Tier:
- 10 hours per month included
- 10 concurrent browsers included
- $0.09 per additional browser hour
- $2.00 per additional concurrent browser
- Timeout extendable to 10 minutes with keep_alive
See references/pricing-and-limits.md for detailed breakdown.
---
Learn More
- SKILL.md: Complete Puppeteer/Playwright API reference
- templates/: 8 ready-to-use code templates
- references/: Deep-dive guides for advanced topics
---
Status: Production Ready ✅ Last Updated: 2025-10-22 Maintainer: Jeremy Dawes (Jezweb)
{
"description": "|",
"metadata": {
"license": "MIT"
},
"references": {
"files": [
"references/common-errors.md",
"references/pricing-and-limits.md",
"references/puppeteer-vs-playwright.md",
"references/session-management.md"
]
},
"content": "Production-ready knowledge domain for building browser automation workflows with Cloudflare Browser Rendering.\r\n\r\n**Status**: Production Ready ✅\r\n**Last Updated**: 2025-10-22\r\n**Dependencies**: cloudflare-worker-base (for Worker setup)\r\n**Latest Versions**: @cloudflare/puppeteer@1.0.4, @cloudflare/playwright@1.0.0, wrangler@4.43.0\r\n\r\n---\r\n\r\n\r\n### Templates (templates/)\r\n\r\nReady-to-use code templates for common patterns:\r\n\r\n- `basic-screenshot.ts` - Minimal screenshot example\r\n- `screenshot-with-kv-cache.ts` - Screenshot with KV caching\r\n- `pdf-generation.ts` - Generate PDFs from HTML or URLs\r\n- `web-scraper-basic.ts` - Basic web scraping pattern\r\n- `web-scraper-batch.ts` - Batch scrape multiple URLs\r\n- `session-reuse.ts` - Session reuse for performance\r\n- `ai-enhanced-scraper.ts` - Scraping with Workers AI\r\n- `playwright-example.ts` - Playwright alternative example\r\n- `wrangler-browser-config.jsonc` - Browser binding configuration\r\n\r\n**Usage:**\r\n```bash",
"name": "cloudflare-browser-rendering",
"id": "cloudflare-browser-rendering",
"sections": {
"Table of Contents": "1. [Quick Start (5 minutes)](#quick-start-5-minutes)\r\n2. [Browser Rendering Overview](#browser-rendering-overview)\r\n3. [Puppeteer API Reference](#puppeteer-api-reference)\r\n4. [Playwright API Reference](#playwright-api-reference)\r\n5. [Session Management](#session-management)\r\n6. [Common Patterns](#common-patterns)\r\n7. [Pricing & Limits](#pricing--limits)\r\n8. [Known Issues Prevention](#known-issues-prevention)\r\n9. [Production Checklist](#production-checklist)\r\n\r\n---",
"Quick Start (5 minutes)": "### 1. Add Browser Binding\r\n\r\n**wrangler.jsonc:**\r\n```jsonc\r\n{\r\n \"name\": \"browser-worker\",\r\n \"main\": \"src/index.ts\",\r\n \"compatibility_date\": \"2023-03-14\",\r\n \"compatibility_flags\": [\"nodejs_compat\"],\r\n \"browser\": {\r\n \"binding\": \"MYBROWSER\"\r\n }\r\n}\r\n```\r\n\r\n**Why nodejs_compat?** Browser Rendering requires Node.js APIs and polyfills.\r\n\r\n### 2. Install Puppeteer\r\n\r\n```bash\r\nnpm install @cloudflare/puppeteer\r\n```\r\n\r\n### 3. Take Your First Screenshot\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\ninterface Env {\r\n MYBROWSER: Fetcher;\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n const { searchParams } = new URL(request.url);\r\n const url = searchParams.get(\"url\") || \"https://example.com\";\r\n\r\n // Launch browser\r\n const browser = await puppeteer.launch(env.MYBROWSER);\r\n const page = await browser.newPage();\r\n\r\n // Navigate and capture\r\n await page.goto(url);\r\n const screenshot = await page.screenshot();\r\n\r\n // Clean up\r\n await browser.close();\r\n\r\n return new Response(screenshot, {\r\n headers: { \"content-type\": \"image/png\" }\r\n });\r\n }\r\n};\r\n```\r\n\r\n### 4. Deploy\r\n\r\n```bash\r\nnpx wrangler deploy\r\n```\r\n\r\nTest at: `https://your-worker.workers.dev/?url=https://example.com`\r\n\r\n**CRITICAL:**\r\n- Always pass `env.MYBROWSER` to `puppeteer.launch()` (not undefined)\r\n- Always call `browser.close()` when done (or use `browser.disconnect()` for session reuse)\r\n- Use `nodejs_compat` compatibility flag\r\n\r\n---",
"Known Issues Prevention": "This skill prevents **6 documented issues**:\r\n\r\n---\r\n\r\n### Issue #1: XPath Selectors Not Supported\r\n\r\n**Error:** \"XPath selector not supported\" or selector failures\r\n**Source:** https://developers.cloudflare.com/browser-rendering/faq/#why-cant-i-use-an-xpath-selector-when-using-browser-rendering-with-puppeteer\r\n**Why It Happens:** XPath poses a security risk to Workers\r\n**Prevention:** Use CSS selectors or `page.evaluate()` with XPathEvaluator\r\n\r\n**Solution:**\r\n```typescript\r\n// ❌ Don't use XPath directly (not supported)\r\n// await page.$x('/html/body/div/h1')\r\n\r\n// ✅ Use CSS selector\r\nconst heading = await page.$(\"div > h1\");\r\n\r\n// ✅ Or use XPath in page.evaluate()\r\nconst innerHtml = await page.evaluate(() => {\r\n return new XPathEvaluator()\r\n .createExpression(\"/html/body/div/h1\")\r\n .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)\r\n .singleNodeValue.innerHTML;\r\n});\r\n```\r\n\r\n---\r\n\r\n### Issue #2: Browser Binding Not Passed\r\n\r\n**Error:** \"Cannot read properties of undefined (reading 'fetch')\"\r\n**Source:** https://developers.cloudflare.com/browser-rendering/faq/#cannot-read-properties-of-undefined-reading-fetch\r\n**Why It Happens:** `puppeteer.launch()` called without browser binding\r\n**Prevention:** Always pass `env.MYBROWSER` to launch\r\n\r\n**Solution:**\r\n```typescript\r\n// ❌ Missing browser binding\r\nconst browser = await puppeteer.launch(); // Error!\r\n\r\n// ✅ Pass binding\r\nconst browser = await puppeteer.launch(env.MYBROWSER);\r\n```\r\n\r\n---\r\n\r\n### Issue #3: Browser Timeout (60 seconds)\r\n\r\n**Error:** Browser closes unexpectedly after 60 seconds\r\n**Source:** https://developers.cloudflare.com/browser-rendering/platform/limits/#note-on-browser-timeout\r\n**Why It Happens:** Default timeout is 60 seconds of inactivity\r\n**Prevention:** Use `keep_alive` option to extend up to 10 minutes\r\n\r\n**Solution:**\r\n```typescript\r\n// Extend timeout to 5 minutes for long-running tasks\r\nconst browser = await puppeteer.launch(env.MYBROWSER, {\r\n keep_alive: 300000 // 5 minutes = 300,000 ms\r\n});\r\n```\r\n\r\n**Note:** Browser closes if no devtools commands for the specified duration.\r\n\r\n---\r\n\r\n### Issue #4: Concurrency Limits Reached\r\n\r\n**Error:** \"Rate limit exceeded\" or new browser launch fails\r\n**Source:** https://developers.cloudflare.com/browser-rendering/platform/limits/\r\n**Why It Happens:** Exceeded concurrent browser limit (3 free, 10-30 paid)\r\n**Prevention:** Reuse sessions, use tabs instead of multiple browsers, check limits before launching\r\n\r\n**Solutions:**\r\n```typescript\r\n// 1. Check limits before launching\r\nconst limits = await puppeteer.limits(env.MYBROWSER);\r\nif (limits.allowedBrowserAcquisitions === 0) {\r\n return new Response(\"Concurrency limit reached\", { status: 429 });\r\n}\r\n\r\n// 2. Reuse sessions\r\nconst sessions = await puppeteer.sessions(env.MYBROWSER);\r\nconst freeSessions = sessions.filter(s => !s.connectionId);\r\nif (freeSessions.length > 0) {\r\n const browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);\r\n}\r\n\r\n// 3. Use tabs instead of multiple browsers\r\nconst browser = await puppeteer.launch(env.MYBROWSER);\r\nconst page1 = await browser.newPage();\r\nconst page2 = await browser.newPage(); // Same browser, different tabs\r\n```\r\n\r\n---\r\n\r\n### Issue #5: Local Development Request Size Limit\r\n\r\n**Error:** Request larger than 1MB fails in `wrangler dev`\r\n**Source:** https://developers.cloudflare.com/browser-rendering/faq/#does-local-development-support-all-browser-rendering-features\r\n**Why It Happens:** Local development limitation\r\n**Prevention:** Use `remote: true` in browser binding for local dev\r\n\r\n**Solution:**\r\n```jsonc\r\n// wrangler.jsonc for local development\r\n{\r\n \"browser\": {\r\n \"binding\": \"MYBROWSER\",\r\n \"remote\": true // Use real headless browser during dev\r\n }\r\n}\r\n```\r\n\r\n---\r\n\r\n### Issue #6: Bot Protection Always Triggered\r\n\r\n**Error:** Website blocks requests as bot traffic\r\n**Source:** https://developers.cloudflare.com/browser-rendering/faq/#will-browser-rendering-bypass-cloudflares-bot-protection\r\n**Why It Happens:** Browser Rendering requests always identified as bots\r\n**Prevention:** Cannot bypass; if scraping your own zone, create WAF skip rule\r\n\r\n**Solution:**\r\n```typescript\r\n// ❌ Cannot bypass bot protection\r\n// Requests will always be identified as bots\r\n\r\n// ✅ If scraping your own Cloudflare zone:\r\n// 1. Go to Security > WAF > Custom rules\r\n// 2. Create skip rule with custom header:\r\n// Header: X-Custom-Auth\r\n// Value: your-secret-token\r\n// 3. Pass header in your scraping requests\r\n\r\n// Note: Automatic headers are included:\r\n// - cf-biso-request-id\r\n// - cf-biso-devtools\r\n```\r\n\r\n---",
"Pricing & Limits": "### Free Tier (Workers Free)\r\n\r\n| Feature | Limit |\r\n|---------|-------|\r\n| **Browser Duration** | 10 minutes per day |\r\n| **Concurrent Browsers** | 3 per account |\r\n| **New Browsers per Minute** | 3 per minute |\r\n| **REST API Requests** | 6 per minute |\r\n| **Browser Timeout** | 60 seconds (idle) |\r\n\r\n### Paid Tier (Workers Paid)\r\n\r\n| Feature | Included | Beyond Included |\r\n|---------|----------|-----------------|\r\n| **Browser Duration** | 10 hours per month | $0.09 per additional browser hour |\r\n| **Concurrent Browsers** | 10 (monthly average) | $2.00 per additional concurrent browser |\r\n| **New Browsers per Minute** | 30 per minute | Request higher limit |\r\n| **REST API Requests** | 180 per minute | Request higher limit |\r\n| **Browser Timeout** | 60 seconds (can extend to 10 minutes with `keep_alive`) | - |\r\n| **Max Concurrent Browsers** | 30 per account | Request higher limit |\r\n\r\n### Pricing Calculation\r\n\r\n**Duration Charges:**\r\n- Charged per browser hour\r\n- Rounded to nearest hour at end of billing cycle\r\n- Failed requests (timeouts) are NOT charged\r\n\r\n**Concurrency Charges:**\r\n- Monthly average of daily peak usage\r\n- Example: 10 browsers for 15 days, 20 browsers for 15 days = (10×15 + 20×15) / 30 = 15 average\r\n- 15 average - 10 included = 5 × $2.00 = $10.00\r\n\r\n**Example Monthly Bill:**\r\n- 50 browser hours used: (50 - 10) × $0.09 = $3.60\r\n- 15 concurrent browsers average: (15 - 10) × $2.00 = $10.00\r\n- **Total: $13.60**\r\n\r\n### Rate Limiting\r\n\r\n**Per-Second Rate:**\r\nRate limits are enforced per-second. Example:\r\n- 180 requests per minute = 3 requests per second\r\n- You cannot send all 180 at once; they must be spread evenly\r\n\r\n**Handling Rate Limits:**\r\n```typescript\r\nasync function launchWithRetry(env: Env, maxRetries = 3): Promise<Browser> {\r\n for (let i = 0; i < maxRetries; i++) {\r\n try {\r\n return await puppeteer.launch(env.MYBROWSER);\r\n } catch (error) {\r\n if (i === maxRetries - 1) throw error;\r\n\r\n // Check if rate limited\r\n const limits = await puppeteer.limits(env.MYBROWSER);\r\n if (limits.allowedBrowserAcquisitions === 0) {\r\n // Wait before retry\r\n const delay = limits.timeUntilNextAllowedBrowserAcquisition || 1000;\r\n await new Promise(resolve => setTimeout(resolve, delay));\r\n }\r\n }\r\n }\r\n throw new Error(\"Failed to launch browser\");\r\n}\r\n```\r\n\r\n---",
"Session Management": "### Why Session Management Matters\r\n\r\n**Problem**: Launching new browsers is slow and consumes concurrency limits.\r\n\r\n**Solution**: Reuse browser sessions across requests.\r\n\r\n**Benefits:**\r\n- ⚡ Faster (no cold start)\r\n- 💰 Lower concurrency usage\r\n- 📊 Better resource utilization\r\n\r\n---\r\n\r\n### Session Reuse Pattern\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\nasync function getBrowser(env: Env): Promise<{ browser: Browser; launched: boolean }> {\r\n // Check for available sessions\r\n const sessions = await puppeteer.sessions(env.MYBROWSER);\r\n const freeSessions = sessions.filter(s => !s.connectionId);\r\n\r\n if (freeSessions.length > 0) {\r\n // Reuse existing session\r\n try {\r\n const browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);\r\n return { browser, launched: false };\r\n } catch (e) {\r\n console.log(\"Failed to connect, launching new browser\");\r\n }\r\n }\r\n\r\n // Launch new session\r\n const browser = await puppeteer.launch(env.MYBROWSER);\r\n return { browser, launched: true };\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n const { browser, launched } = await getBrowser(env);\r\n\r\n try {\r\n const page = await browser.newPage();\r\n await page.goto(\"https://example.com\");\r\n const screenshot = await page.screenshot();\r\n\r\n // Disconnect (don't close) to allow reuse\r\n await browser.disconnect();\r\n\r\n return new Response(screenshot, {\r\n headers: { \"content-type\": \"image/png\" }\r\n });\r\n } catch (error) {\r\n await browser.close(); // Close on error\r\n throw error;\r\n }\r\n }\r\n};\r\n```\r\n\r\n**CRITICAL:**\r\n- Use `browser.disconnect()` to keep session alive\r\n- Use `browser.close()` on errors\r\n- Always handle connection failures\r\n\r\n---\r\n\r\n### Incognito Browser Contexts\r\n\r\nUse browser contexts to isolate cookies and cache while sharing a browser instance.\r\n\r\n**Benefits:**\r\n- Share browser (reduce concurrency)\r\n- Isolate sessions (separate cookies/cache)\r\n- Test multi-user scenarios\r\n\r\n**Example:**\r\n```typescript\r\nconst browser = await puppeteer.launch(env.MYBROWSER);\r\n\r\n// Create isolated contexts\r\nconst context1 = await browser.createBrowserContext();\r\nconst context2 = await browser.createBrowserContext();\r\n\r\n// Each context has its own cookies/cache\r\nconst page1 = await context1.newPage();\r\nconst page2 = await context2.newPage();\r\n\r\nawait page1.goto(\"https://app.example.com\"); // User 1\r\nawait page2.goto(\"https://app.example.com\"); // User 2\r\n\r\nawait context1.close();\r\nawait context2.close();\r\nawait browser.close();\r\n```\r\n\r\n---\r\n\r\n### Multiple Tabs vs Multiple Browsers\r\n\r\n**Scenario**: Scrape 10 URLs\r\n\r\n**❌ Bad (10 browsers):**\r\n```typescript\r\nfor (const url of urls) {\r\n const browser = await puppeteer.launch(env.MYBROWSER); // 10 launches!\r\n // ... scrape ...\r\n await browser.close();\r\n}\r\n```\r\n\r\n**✅ Good (1 browser, 10 tabs):**\r\n```typescript\r\nconst browser = await puppeteer.launch(env.MYBROWSER);\r\n\r\nconst results = await Promise.all(\r\n urls.map(async (url) => {\r\n const page = await browser.newPage();\r\n await page.goto(url);\r\n const data = await page.evaluate(() => ({\r\n title: document.title,\r\n text: document.body.innerText\r\n }));\r\n await page.close();\r\n return { url, data };\r\n })\r\n);\r\n\r\nawait browser.close();\r\n```\r\n\r\n**Benefit**: Uses 1 concurrent browser instead of 10.\r\n\r\n---",
"Dependencies": "**Required:**\r\n- `@cloudflare/puppeteer@1.0.4` - Puppeteer for Workers\r\n- `wrangler@4.43.0+` - Cloudflare CLI\r\n\r\n**Optional:**\r\n- `@cloudflare/playwright@1.0.0` - Playwright for Workers (alternative)\r\n- `@cloudflare/workers-types@4.20251014.0+` - TypeScript types\r\n\r\n**Related Skills:**\r\n- `cloudflare-worker-base` - Worker setup with Hono\r\n- `cloudflare-kv` - KV caching for screenshots\r\n- `cloudflare-r2` - R2 storage for generated files\r\n- `cloudflare-workers-ai` - AI-enhanced scraping\r\n\r\n---",
"Troubleshooting": "### Problem: \"Cannot read properties of undefined (reading 'fetch')\"\r\n**Solution:** Pass browser binding to puppeteer.launch():\r\n```typescript\r\nconst browser = await puppeteer.launch(env.MYBROWSER); // Not just puppeteer.launch()\r\n```\r\n\r\n### Problem: XPath selectors not working\r\n**Solution:** Use CSS selectors or page.evaluate() with XPathEvaluator (see Issue #1)\r\n\r\n### Problem: Browser closes after 60 seconds\r\n**Solution:** Extend timeout with keep_alive:\r\n```typescript\r\nconst browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 300000 });\r\n```\r\n\r\n### Problem: Rate limit reached\r\n**Solution:** Reuse sessions, use tabs, check limits before launching (see Issue #4)\r\n\r\n### Problem: Local dev request > 1MB fails\r\n**Solution:** Enable remote binding in wrangler.jsonc:\r\n```jsonc\r\n{ \"browser\": { \"binding\": \"MYBROWSER\", \"remote\": true } }\r\n```\r\n\r\n### Problem: Website blocks as bot\r\n**Solution:** Cannot bypass. If your own zone, create WAF skip rule (see Issue #6)\r\n\r\n---\r\n\r\n**Questions? Issues?**\r\n\r\n1. Check `references/common-errors.md` for detailed solutions\r\n2. Review `references/session-management.md` for performance optimization\r\n3. Verify browser binding is configured in wrangler.jsonc\r\n4. Check official docs: https://developers.cloudflare.com/browser-rendering/\r\n5. Ensure `nodejs_compat` compatibility flag is enabled",
"Playwright API Reference": "Playwright provides a similar API to Puppeteer with slight differences.\r\n\r\n### Installation\r\n\r\n```bash\r\nnpm install @cloudflare/playwright\r\n```\r\n\r\n### Basic Example\r\n\r\n```typescript\r\nimport { env } from \"cloudflare:test\";\r\nimport { chromium } from \"@cloudflare/playwright\";\r\n\r\ninterface Env {\r\n BROWSER: Fetcher;\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n const browser = await chromium.launch(env.BROWSER);\r\n const page = await browser.newPage();\r\n\r\n await page.goto(\"https://example.com\");\r\n const screenshot = await page.screenshot();\r\n\r\n await browser.close();\r\n\r\n return new Response(screenshot, {\r\n headers: { \"content-type\": \"image/png\" }\r\n });\r\n }\r\n};\r\n```\r\n\r\n### Key Differences from Puppeteer\r\n\r\n| Feature | Puppeteer | Playwright |\r\n|---------|-----------|------------|\r\n| **Import** | `import puppeteer from \"@cloudflare/puppeteer\"` | `import { chromium } from \"@cloudflare/playwright\"` |\r\n| **Launch** | `puppeteer.launch(env.MYBROWSER)` | `chromium.launch(env.BROWSER)` |\r\n| **Session API** | ✅ Advanced (sessions, history, limits) | ⚠️ Basic |\r\n| **Auto-waiting** | Manual `waitForSelector()` | Built-in auto-waiting |\r\n| **Selectors** | CSS only | CSS, text, XPath (via evaluate workaround) |\r\n\r\n**Recommendation**: Stick with Puppeteer unless you have existing Playwright tests to migrate.\r\n\r\n---",
"Error Handling Template": "Complete error handling for production use:\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\ninterface Env {\r\n MYBROWSER: Fetcher;\r\n}\r\n\r\nasync function withBrowser<T>(\r\n env: Env,\r\n fn: (browser: Browser) => Promise<T>\r\n): Promise<T> {\r\n let browser: Browser | null = null;\r\n\r\n try {\r\n // Check limits\r\n const limits = await puppeteer.limits(env.MYBROWSER);\r\n if (limits.allowedBrowserAcquisitions === 0) {\r\n throw new Error(\"Rate limit reached. Retry after: \" + limits.timeUntilNextAllowedBrowserAcquisition + \"ms\");\r\n }\r\n\r\n // Launch or connect\r\n const sessions = await puppeteer.sessions(env.MYBROWSER);\r\n const freeSessions = sessions.filter(s => !s.connectionId);\r\n\r\n if (freeSessions.length > 0) {\r\n try {\r\n browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);\r\n } catch {\r\n browser = await puppeteer.launch(env.MYBROWSER);\r\n }\r\n } else {\r\n browser = await puppeteer.launch(env.MYBROWSER);\r\n }\r\n\r\n // Execute user function\r\n const result = await fn(browser);\r\n\r\n // Disconnect (keep session alive)\r\n await browser.disconnect();\r\n\r\n return result;\r\n } catch (error) {\r\n // Close on error\r\n if (browser) {\r\n await browser.close();\r\n }\r\n throw error;\r\n }\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n try {\r\n const screenshot = await withBrowser(env, async (browser) => {\r\n const page = await browser.newPage();\r\n await page.goto(\"https://example.com\", {\r\n waitUntil: \"networkidle0\",\r\n timeout: 30000\r\n });\r\n return await page.screenshot();\r\n });\r\n\r\n return new Response(screenshot, {\r\n headers: { \"content-type\": \"image/png\" }\r\n });\r\n } catch (error) {\r\n console.error(\"Browser error:\", error);\r\n return new Response(\r\n JSON.stringify({\r\n error: error instanceof Error ? error.message : \"Unknown error\"\r\n }),\r\n { status: 500, headers: { \"content-type\": \"application/json\" } }\r\n );\r\n }\r\n }\r\n};\r\n```\r\n\r\n---",
"Common Patterns": "### Pattern 1: Screenshot with KV Caching\r\n\r\nCache screenshots to reduce browser usage and improve performance.\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\ninterface Env {\r\n MYBROWSER: Fetcher;\r\n CACHE: KVNamespace;\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n const { searchParams } = new URL(request.url);\r\n const url = searchParams.get(\"url\");\r\n\r\n if (!url) {\r\n return new Response(\"Missing ?url parameter\", { status: 400 });\r\n }\r\n\r\n const normalizedUrl = new URL(url).toString();\r\n\r\n // Check cache\r\n let screenshot = await env.CACHE.get(normalizedUrl, { type: \"arrayBuffer\" });\r\n\r\n if (!screenshot) {\r\n // Generate screenshot\r\n const browser = await puppeteer.launch(env.MYBROWSER);\r\n const page = await browser.newPage();\r\n await page.goto(normalizedUrl);\r\n screenshot = await page.screenshot();\r\n await browser.close();\r\n\r\n // Cache for 24 hours\r\n await env.CACHE.put(normalizedUrl, screenshot, {\r\n expirationTtl: 60 * 60 * 24\r\n });\r\n }\r\n\r\n return new Response(screenshot, {\r\n headers: { \"content-type\": \"image/png\" }\r\n });\r\n }\r\n};\r\n```\r\n\r\n---\r\n\r\n### Pattern 2: PDF Generation from HTML\r\n\r\nConvert custom HTML to PDF.\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\ninterface Env {\r\n MYBROWSER: Fetcher;\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n if (request.method !== \"POST\") {\r\n return new Response(\"Method not allowed\", { status: 405 });\r\n }\r\n\r\n const { html } = await request.json<{ html: string }>();\r\n\r\n const browser = await puppeteer.launch(env.MYBROWSER);\r\n const page = await browser.newPage();\r\n\r\n // Set custom HTML\r\n await page.setContent(html, { waitUntil: \"networkidle0\" });\r\n\r\n // Generate PDF\r\n const pdf = await page.pdf({\r\n format: \"A4\",\r\n printBackground: true,\r\n margin: {\r\n top: \"1cm\",\r\n right: \"1cm\",\r\n bottom: \"1cm\",\r\n left: \"1cm\"\r\n }\r\n });\r\n\r\n await browser.close();\r\n\r\n return new Response(pdf, {\r\n headers: {\r\n \"content-type\": \"application/pdf\",\r\n \"content-disposition\": \"attachment; filename=document.pdf\"\r\n }\r\n });\r\n }\r\n};\r\n```\r\n\r\n---\r\n\r\n### Pattern 3: Web Scraping with Structured Data\r\n\r\nExtract structured data from web pages.\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\ninterface Env {\r\n MYBROWSER: Fetcher;\r\n}\r\n\r\ninterface ProductData {\r\n title: string;\r\n price: string;\r\n description: string;\r\n image: string;\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n const { searchParams } = new URL(request.url);\r\n const url = searchParams.get(\"url\");\r\n\r\n const browser = await puppeteer.launch(env.MYBROWSER);\r\n const page = await browser.newPage();\r\n\r\n await page.goto(url!, { waitUntil: \"networkidle0\" });\r\n\r\n // Extract structured data\r\n const data = await page.evaluate<ProductData>(() => {\r\n return {\r\n title: document.querySelector(\"h1\")?.textContent || \"\",\r\n price: document.querySelector(\".price\")?.textContent || \"\",\r\n description: document.querySelector(\".description\")?.textContent || \"\",\r\n image: document.querySelector(\"img\")?.src || \"\"\r\n };\r\n });\r\n\r\n await browser.close();\r\n\r\n return Response.json({ url, data });\r\n }\r\n};\r\n```\r\n\r\n---\r\n\r\n### Pattern 4: Batch Scraping Multiple URLs\r\n\r\nEfficiently scrape multiple URLs using tabs.\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\ninterface Env {\r\n MYBROWSER: Fetcher;\r\n}\r\n\r\nasync function scrapeUrl(browser: Browser, url: string): Promise<any> {\r\n const page = await browser.newPage();\r\n try {\r\n await page.goto(url, { waitUntil: \"networkidle0\", timeout: 30000 });\r\n\r\n const data = await page.evaluate(() => ({\r\n title: document.title,\r\n url: window.location.href,\r\n text: document.body.innerText.slice(0, 500) // First 500 chars\r\n }));\r\n\r\n await page.close();\r\n return { success: true, url, data };\r\n } catch (error) {\r\n await page.close();\r\n return {\r\n success: false,\r\n url,\r\n error: error instanceof Error ? error.message : \"Unknown error\"\r\n };\r\n }\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n const { urls } = await request.json<{ urls: string[] }>();\r\n\r\n if (!urls || urls.length === 0) {\r\n return new Response(\"Missing urls array\", { status: 400 });\r\n }\r\n\r\n const browser = await puppeteer.launch(env.MYBROWSER);\r\n\r\n // Scrape all URLs in parallel (each in its own tab)\r\n const results = await Promise.all(\r\n urls.map(url => scrapeUrl(browser, url))\r\n );\r\n\r\n await browser.close();\r\n\r\n return Response.json({ results });\r\n }\r\n};\r\n```\r\n\r\n---\r\n\r\n### Pattern 5: AI-Enhanced Scraping\r\n\r\nCombine Browser Rendering with Workers AI to extract structured data.\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\ninterface Env {\r\n MYBROWSER: Fetcher;\r\n AI: Ai;\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n const { searchParams } = new URL(request.url);\r\n const url = searchParams.get(\"url\");\r\n\r\n // Scrape page content\r\n const browser = await puppeteer.launch(env.MYBROWSER);\r\n const page = await browser.newPage();\r\n await page.goto(url!, { waitUntil: \"networkidle0\" });\r\n\r\n const bodyContent = await page.$eval(\"body\", el => el.innerHTML);\r\n await browser.close();\r\n\r\n // Extract structured data with AI\r\n const response = await env.AI.run(\"@cf/meta/llama-3.1-8b-instruct\", {\r\n messages: [\r\n {\r\n role: \"user\",\r\n content: `Extract product information as JSON from this HTML. Include: name, price, description, availability.\\n\\nHTML:\\n${bodyContent.slice(0, 4000)}`\r\n }\r\n ]\r\n });\r\n\r\n // Parse AI response\r\n let productData;\r\n try {\r\n productData = JSON.parse(response.response);\r\n } catch {\r\n productData = { raw: response.response };\r\n }\r\n\r\n return Response.json({ url, product: productData });\r\n }\r\n};\r\n```\r\n\r\n---\r\n\r\n### Pattern 6: Form Filling and Automation\r\n\r\nAutomate form submissions and multi-step workflows.\r\n\r\n```typescript\r\nimport puppeteer from \"@cloudflare/puppeteer\";\r\n\r\ninterface Env {\r\n MYBROWSER: Fetcher;\r\n}\r\n\r\nexport default {\r\n async fetch(request: Request, env: Env): Promise<Response> {\r\n const { email, password } = await request.json<{\r\n email: string;\r\n password: string;\r\n }>();\r\n\r\n const browser = await puppeteer.launch(env.MYBROWSER);\r\n const page = await browser.newPage();\r\n\r\n // Navigate to login page\r\n await page.goto(\"https://example.com/login\");\r\n\r\n // Fill form\r\n await page.type('input[name=\"email\"]', email);\r\n await page.type('input[name=\"password\"]', password);\r\n\r\n // Submit and wait for navigation\r\n await page.click('button[type=\"submit\"]');\r\n await page.waitForNavigation();\r\n\r\n // Extract result\r\n const result = await page.evaluate(() => ({\r\n url: window.location.href,\r\n title: document.title,\r\n loggedIn: document.querySelector(\".user-profile\") !== null\r\n }));\r\n\r\n await browser.close();\r\n\r\n return Response.json(result);\r\n }\r\n};\r\n```\r\n\r\n---",
"Browser Rendering Overview": "### What is Browser Rendering?\r\n\r\nCloudflare Browser Rendering provides headless Chromium browsers running on Cloudflare's global network. Use familiar tools like Puppeteer and Playwright to automate browser tasks:\r\n\r\n- **Screenshots** - Capture visual snapshots of web pages\r\n- **PDF Generation** - Convert HTML/URLs to PDFs\r\n- **Web Scraping** - Extract content from dynamic websites\r\n- **Testing** - Automate frontend tests\r\n- **Crawling** - Navigate multi-page workflows\r\n\r\n### Two Integration Methods\r\n\r\n| Method | Best For | Complexity |\r\n|--------|----------|-----------|\r\n| **Workers Bindings** | Complex automation, custom workflows, session management | Advanced |\r\n| **REST API** | Simple screenshot/PDF tasks | Simple |\r\n\r\n**This skill covers Workers Bindings** (the advanced method with full Puppeteer/Playwright APIs).\r\n\r\n### Puppeteer vs Playwright\r\n\r\n| Feature | Puppeteer | Playwright |\r\n|---------|-----------|------------|\r\n| **API Familiarity** | Most popular | Growing adoption |\r\n| **Package** | `@cloudflare/puppeteer@1.0.4` | `@cloudflare/playwright@1.0.0` |\r\n| **Session Management** | ✅ Advanced APIs | ⚠️ Basic |\r\n| **Browser Support** | Chromium only | Chromium only (Firefox/Safari not yet supported) |\r\n| **Best For** | Screenshots, PDFs, scraping | Testing, frontend automation |\r\n\r\n**Recommendation**: Use Puppeteer for most use cases. Playwright is ideal if you're already using it for testing.\r\n\r\n---",
"Official Documentation": "- **Browser Rendering Docs**: https://developers.cloudflare.com/browser-rendering/\r\n- **Puppeteer API**: https://pptr.dev/api/\r\n- **Playwright API**: https://playwright.dev/docs/api/class-playwright\r\n- **Cloudflare Puppeteer Fork**: https://github.com/cloudflare/puppeteer\r\n- **Cloudflare Playwright Fork**: https://github.com/cloudflare/playwright\r\n- **Pricing**: https://developers.cloudflare.com/browser-rendering/platform/pricing/\r\n- **Limits**: https://developers.cloudflare.com/browser-rendering/platform/limits/\r\n\r\n---",
"Package Versions (Verified 2025-10-22)": "```json\r\n{\r\n \"dependencies\": {\r\n \"@cloudflare/puppeteer\": \"^1.0.4\"\r\n },\r\n \"devDependencies\": {\r\n \"@cloudflare/workers-types\": \"^4.20251014.0\",\r\n \"wrangler\": \"^4.43.0\"\r\n }\r\n}\r\n```\r\n\r\n**Alternative (Playwright):**\r\n```json\r\n{\r\n \"dependencies\": {\r\n \"@cloudflare/playwright\": \"^1.0.0\"\r\n }\r\n}\r\n```\r\n\r\n---",
"Production Checklist": "Before deploying Browser Rendering Workers to production:\r\n\r\n### Configuration\r\n- [ ] **Browser binding configured** in wrangler.jsonc\r\n- [ ] **nodejs_compat flag enabled** (required for Browser Rendering)\r\n- [ ] **Keep-alive timeout set** if tasks take > 60 seconds\r\n- [ ] **Remote binding enabled** for local development if needed\r\n\r\n### Error Handling\r\n- [ ] **Retry logic implemented** for rate limits\r\n- [ ] **Timeout handling** for page.goto()\r\n- [ ] **Browser cleanup** in try-finally blocks\r\n- [ ] **Concurrency limit checks** before launching browsers\r\n- [ ] **Graceful degradation** when browser unavailable\r\n\r\n### Performance\r\n- [ ] **Session reuse implemented** for high-traffic routes\r\n- [ ] **Multiple tabs used** instead of multiple browsers\r\n- [ ] **Incognito contexts** for session isolation\r\n- [ ] **KV caching** for repeated screenshots/PDFs\r\n- [ ] **Batch operations** to maximize browser utilization\r\n\r\n### Monitoring\r\n- [ ] **Log browser session IDs** for debugging\r\n- [ ] **Track browser duration** for billing estimates\r\n- [ ] **Monitor concurrency usage** with puppeteer.limits()\r\n- [ ] **Alert on rate limit errors**\r\n- [ ] **Dashboard monitoring** at https://dash.cloudflare.com/?to=/:account/workers/browser-rendering\r\n\r\n### Security\r\n- [ ] **Input validation** for URLs (prevent SSRF)\r\n- [ ] **Timeout limits** to prevent abuse\r\n- [ ] **Rate limiting** on public endpoints\r\n- [ ] **Authentication** for sensitive scraping endpoints\r\n- [ ] **WAF rules** if scraping your own zone\r\n\r\n### Testing\r\n- [ ] **Test screenshot capture** with various page sizes\r\n- [ ] **Test PDF generation** with custom HTML\r\n- [ ] **Test scraping** with dynamic content (networkidle0)\r\n- [ ] **Test error scenarios** (invalid URLs, timeouts)\r\n- [ ] **Load test** concurrency limits\r\n\r\n---",
"Using Bundled Resources": "cp ~/.claude/skills/cloudflare-browser-rendering/templates/basic-screenshot.ts src/index.ts\r\n```\r\n\r\n### References (references/)\r\n\r\nDeep-dive documentation:\r\n\r\n- `session-management.md` - Complete session reuse guide\r\n- `pricing-and-limits.md` - Detailed pricing breakdown\r\n- `common-errors.md` - All known issues and solutions\r\n- `puppeteer-vs-playwright.md` - Feature comparison and migration\r\n\r\n**When to load:** Reference when implementing advanced patterns or debugging specific issues.\r\n\r\n---",
"Puppeteer API Reference": "### puppeteer.launch()\r\n\r\nLaunch a new browser instance.\r\n\r\n**Signature:**\r\n```typescript\r\nawait puppeteer.launch(binding: Fetcher, options?: LaunchOptions): Promise<Browser>\r\n```\r\n\r\n**Parameters:**\r\n- `binding` (required) - Browser binding from `env.MYBROWSER`\r\n- `options` (optional):\r\n - `keep_alive` (number) - Keep browser alive for N milliseconds (max: 600000 = 10 minutes)\r\n\r\n**Returns:** `Promise<Browser>` - Browser instance\r\n\r\n**Example:**\r\n```typescript\r\nconst browser = await puppeteer.launch(env.MYBROWSER, {\r\n keep_alive: 60000 // Keep alive for 60 seconds\r\n});\r\n```\r\n\r\n**CRITICAL:** Must pass `env.MYBROWSER` binding. Error \"Cannot read properties of undefined (reading 'fetch')\" means the binding wasn't passed.\r\n\r\n---\r\n\r\n### puppeteer.connect()\r\n\r\nConnect to an existing browser session.\r\n\r\n**Signature:**\r\n```typescript\r\nawait puppeteer.connect(binding: Fetcher, sessionId: string): Promise<Browser>\r\n```\r\n\r\n**Use Cases:**\r\n- Reuse existing browser sessions for performance\r\n- Share browser instance across multiple Workers\r\n- Reduce startup time\r\n\r\n**Example:**\r\n```typescript\r\nconst sessionId = \"478f4d7d-e943-40f6-a414-837d3736a1dc\";\r\nconst browser = await puppeteer.connect(env.MYBROWSER, sessionId);\r\n```\r\n\r\n---\r\n\r\n### puppeteer.sessions()\r\n\r\nList currently running browser sessions.\r\n\r\n**Signature:**\r\n```typescript\r\nawait puppeteer.sessions(binding: Fetcher): Promise<SessionInfo[]>\r\n```\r\n\r\n**Returns:**\r\n```typescript\r\ninterface SessionInfo {\r\n sessionId: string;\r\n startTime: number;\r\n connectionId?: string; // Present if worker is connected\r\n connectionStartTime?: number;\r\n}\r\n```\r\n\r\n**Example:**\r\n```typescript\r\nconst sessions = await puppeteer.sessions(env.MYBROWSER);\r\n// Find sessions without active connections\r\nconst freeSessions = sessions.filter(s => !s.connectionId);\r\n```\r\n\r\n---\r\n\r\n### puppeteer.history()\r\n\r\nList recent sessions (both open and closed).\r\n\r\n**Signature:**\r\n```typescript\r\nawait puppeteer.history(binding: Fetcher): Promise<HistoryEntry[]>\r\n```\r\n\r\n**Returns:**\r\n```typescript\r\ninterface HistoryEntry {\r\n sessionId: string;\r\n startTime: number;\r\n endTime?: number;\r\n closeReason?: number;\r\n closeReasonText?: string; // \"NormalClosure\", \"BrowserIdle\", etc.\r\n}\r\n```\r\n\r\n**Use Case:** Monitor usage patterns and debug session issues.\r\n\r\n---\r\n\r\n### puppeteer.limits()\r\n\r\nCheck current account limits and available sessions.\r\n\r\n**Signature:**\r\n```typescript\r\nawait puppeteer.limits(binding: Fetcher): Promise<LimitsInfo>\r\n```\r\n\r\n**Returns:**\r\n```typescript\r\ninterface LimitsInfo {\r\n activeSessions: Array<{ id: string }>;\r\n maxConcurrentSessions: number;\r\n allowedBrowserAcquisitions: number;\r\n timeUntilNextAllowedBrowserAcquisition: number; // milliseconds\r\n}\r\n```\r\n\r\n**Example:**\r\n```typescript\r\nconst limits = await puppeteer.limits(env.MYBROWSER);\r\nif (limits.allowedBrowserAcquisitions === 0) {\r\n return new Response(\"Rate limit reached\", { status: 429 });\r\n}\r\n```\r\n\r\n---\r\n\r\n### Browser API\r\n\r\nMethods available on the `Browser` object returned by `launch()` or `connect()`.\r\n\r\n#### browser.newPage()\r\n\r\nCreate a new page (tab) in the browser.\r\n\r\n**Signature:**\r\n```typescript\r\nawait browser.newPage(): Promise<Page>\r\n```\r\n\r\n**Example:**\r\n```typescript\r\nconst page = await browser.newPage();\r\nawait page.goto(\"https://example.com\");\r\n```\r\n\r\n**Performance Tip:** Reuse browser instances and open multiple tabs instead of launching new browsers.\r\n\r\n---\r\n\r\n#### browser.sessionId()\r\n\r\nGet the current browser session ID.\r\n\r\n**Returns:** `string` - Session ID\r\n\r\n**Example:**\r\n```typescript\r\nconst sessionId = browser.sessionId();\r\nconsole.log(\"Current session:\", sessionId);\r\n```\r\n\r\n---\r\n\r\n#### browser.close()\r\n\r\nClose the browser and terminate the session.\r\n\r\n**Signature:**\r\n```typescript\r\nawait browser.close(): Promise<void>\r\n```\r\n\r\n**When to use:** When you're completely done with the browser and want to free resources.\r\n\r\n---\r\n\r\n#### browser.disconnect()\r\n\r\nDisconnect from the browser WITHOUT closing it.\r\n\r\n**Signature:**\r\n```typescript\r\nawait browser.disconnect(): Promise<void>\r\n```\r\n\r\n**When to use:** Session reuse - allows another Worker to connect to the same session later.\r\n\r\n**Example:**\r\n```typescript\r\n// Keep session alive for reuse\r\nconst sessionId = browser.sessionId();\r\nawait browser.disconnect(); // Don't close, just disconnect\r\n// Later: puppeteer.connect(env.MYBROWSER, sessionId)\r\n```\r\n\r\n---\r\n\r\n#### browser.createBrowserContext()\r\n\r\nCreate an isolated incognito browser context.\r\n\r\n**Signature:**\r\n```typescript\r\nawait browser.createBrowserContext(): Promise<BrowserContext>\r\n```\r\n\r\n**Use Cases:**\r\n- Isolate cookies and cache between operations\r\n- Test multi-user scenarios\r\n- Maintain session isolation while reusing browser\r\n\r\n**Example:**\r\n```typescript\r\nconst context1 = await browser.createBrowserContext();\r\nconst context2 = await browser.createBrowserContext();\r\n\r\nconst page1 = await context1.newPage();\r\nconst page2 = await context2.newPage();\r\n\r\n// page1 and page2 have separate cookies/cache\r\n```\r\n\r\n---\r\n\r\n### Page API\r\n\r\nMethods available on the `Page` object returned by `browser.newPage()`.\r\n\r\n#### page.goto()\r\n\r\nNavigate to a URL.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.goto(url: string, options?: NavigationOptions): Promise<Response>\r\n```\r\n\r\n**Options:**\r\n- `waitUntil` - When to consider navigation complete:\r\n - `\"load\"` - Wait for load event (default)\r\n - `\"domcontentloaded\"` - Wait for DOMContentLoaded\r\n - `\"networkidle0\"` - Wait until no network connections for 500ms\r\n - `\"networkidle2\"` - Wait until ≤2 network connections for 500ms\r\n- `timeout` - Maximum navigation time in milliseconds (default: 30000)\r\n\r\n**Example:**\r\n```typescript\r\nawait page.goto(\"https://example.com\", {\r\n waitUntil: \"networkidle0\",\r\n timeout: 60000\r\n});\r\n```\r\n\r\n**Best Practice:** Use `\"networkidle0\"` for dynamic content, `\"load\"` for static pages.\r\n\r\n---\r\n\r\n#### page.screenshot()\r\n\r\nCapture a screenshot of the page.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.screenshot(options?: ScreenshotOptions): Promise<Buffer>\r\n```\r\n\r\n**Options:**\r\n- `fullPage` (boolean) - Capture full scrollable page (default: false)\r\n- `type` (string) - `\"png\"` or `\"jpeg\"` (default: `\"png\"`)\r\n- `quality` (number) - JPEG quality 0-100 (only for jpeg)\r\n- `clip` (object) - Capture specific region: `{ x, y, width, height }`\r\n\r\n**Examples:**\r\n```typescript\r\n// Full page screenshot\r\nconst screenshot = await page.screenshot({ fullPage: true });\r\n\r\n// JPEG with compression\r\nconst screenshot = await page.screenshot({\r\n type: \"jpeg\",\r\n quality: 80\r\n});\r\n\r\n// Specific region\r\nconst screenshot = await page.screenshot({\r\n clip: { x: 0, y: 0, width: 800, height: 600 }\r\n});\r\n```\r\n\r\n---\r\n\r\n#### page.pdf()\r\n\r\nGenerate a PDF of the page.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.pdf(options?: PDFOptions): Promise<Buffer>\r\n```\r\n\r\n**Options:**\r\n- `format` (string) - Page format: `\"Letter\"`, `\"A4\"`, etc.\r\n- `printBackground` (boolean) - Include background graphics (default: false)\r\n- `margin` (object) - `{ top, right, bottom, left }` (e.g., `\"1cm\"`)\r\n- `landscape` (boolean) - Landscape orientation (default: false)\r\n- `scale` (number) - Scale factor 0.1-2 (default: 1)\r\n\r\n**Example:**\r\n```typescript\r\nconst pdf = await page.pdf({\r\n format: \"A4\",\r\n printBackground: true,\r\n margin: { top: \"1cm\", right: \"1cm\", bottom: \"1cm\", left: \"1cm\" }\r\n});\r\n\r\nreturn new Response(pdf, {\r\n headers: { \"content-type\": \"application/pdf\" }\r\n});\r\n```\r\n\r\n---\r\n\r\n#### page.content()\r\n\r\nGet the full HTML content of the page.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.content(): Promise<string>\r\n```\r\n\r\n**Example:**\r\n```typescript\r\nconst html = await page.content();\r\nconsole.log(html); // Full HTML source\r\n```\r\n\r\n---\r\n\r\n#### page.setContent()\r\n\r\nSet custom HTML content.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.setContent(html: string, options?: NavigationOptions): Promise<void>\r\n```\r\n\r\n**Use Case:** Generate PDFs from custom HTML.\r\n\r\n**Example:**\r\n```typescript\r\nawait page.setContent(`\r\n <!DOCTYPE html>\r\n <html>\r\n <head><style>body { font-family: Arial; }</style></head>\r\n <body><h1>Hello World</h1></body>\r\n </html>\r\n`);\r\n\r\nconst pdf = await page.pdf({ format: \"A4\" });\r\n```\r\n\r\n---\r\n\r\n#### page.evaluate()\r\n\r\nExecute JavaScript in the browser context.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.evaluate<T>(fn: () => T): Promise<T>\r\n```\r\n\r\n**Use Cases:**\r\n- Extract data from the DOM\r\n- Manipulate page content\r\n- Workaround for XPath (not directly supported)\r\n\r\n**Examples:**\r\n```typescript\r\n// Extract text content\r\nconst title = await page.evaluate(() => document.title);\r\n\r\n// Extract structured data\r\nconst data = await page.evaluate(() => ({\r\n title: document.title,\r\n url: window.location.href,\r\n headings: Array.from(document.querySelectorAll(\"h1, h2\")).map(el => el.textContent),\r\n links: Array.from(document.querySelectorAll(\"a\")).map(el => el.href)\r\n}));\r\n\r\n// XPath workaround (XPath selectors not directly supported)\r\nconst innerHtml = await page.evaluate(() => {\r\n return new XPathEvaluator()\r\n .createExpression(\"/html/body/div/h1\")\r\n .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)\r\n .singleNodeValue.innerHTML;\r\n});\r\n```\r\n\r\n---\r\n\r\n#### page.waitForSelector()\r\n\r\nWait for an element to appear in the DOM.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.waitForSelector(selector: string, options?: WaitForOptions): Promise<ElementHandle>\r\n```\r\n\r\n**Options:**\r\n- `timeout` (number) - Maximum wait time in milliseconds\r\n- `visible` (boolean) - Wait for element to be visible\r\n\r\n**Example:**\r\n```typescript\r\nawait page.goto(\"https://example.com\");\r\nawait page.waitForSelector(\"#content\", { visible: true });\r\nconst screenshot = await page.screenshot();\r\n```\r\n\r\n---\r\n\r\n#### page.type()\r\n\r\nType text into an input field.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.type(selector: string, text: string): Promise<void>\r\n```\r\n\r\n**Example:**\r\n```typescript\r\nawait page.type('input[name=\"email\"]', 'user@example.com');\r\n```\r\n\r\n---\r\n\r\n#### page.click()\r\n\r\nClick an element.\r\n\r\n**Signature:**\r\n```typescript\r\nawait page.click(selector: string): Promise<void>\r\n```\r\n\r\n**Example:**\r\n```typescript\r\nawait page.click('button[type=\"submit\"]');\r\nawait page.waitForNavigation();\r\n```\r\n\r\n---"
}
}