---
title: Browser Run
description: Control headless browsers with Cloudflare's Workers Browser Run API. Automate tasks, take screenshots, convert pages to PDFs, and test web apps.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Browser Run

Run headless Chrome on [Cloudflare's global network](https://developers.cloudflare.com/workers/) for browser automation, web scraping, testing, and content generation.

 Available on Free and Paid plans 

Browser Run, formerly known as Browser Rendering, enables developers to programmatically control and interact with headless browser instances running on Cloudflare’s global network.

## Use cases

Programmatically load and fully render dynamic webpages or raw HTML and capture specific outputs such as:

* [Markdown](https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/)
* [Screenshots](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/)
* [PDFs](https://developers.cloudflare.com/browser-run/quick-actions/pdf-endpoint/)
* [Snapshots](https://developers.cloudflare.com/browser-run/quick-actions/snapshot/)
* [Links](https://developers.cloudflare.com/browser-run/quick-actions/links-endpoint/)
* [HTML elements](https://developers.cloudflare.com/browser-run/quick-actions/scrape-endpoint/)
* [Structured data](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/)
* [Crawled web content](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/)

## Integration methods

Browser Run offers two categories of integration methods:

* **[Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/)**: Simple, stateless browser tasks like screenshots, PDFs, and scraping. No code deployment needed.
* **Browser Sessions**: Direct browser control via [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), [CDP](https://developers.cloudflare.com/browser-run/cdp/), or [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/). Deploy within Cloudflare Workers or connect from any environment via CDP.

| Use case                                    | Recommended                                                                                                                                                                                                  | Why                                                              |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| Simple screenshot, PDF, or scrape           | [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/)                                                                                                                                | No code deployment; single HTTP request                          |
| Browser automation                          | [Playwright](https://developers.cloudflare.com/browser-run/playwright/), [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/) | Full browser control with scripting                              |
| Porting existing scripts                    | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/) | Minimal code changes from standard libraries                     |
| AI-powered data extraction                  | [JSON endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/)                                                                                                                  | Structured data via natural language prompts                     |
| Site-wide crawling                          | [Crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/)                                                                                                                | Multi-page content extraction with async results                 |
| AI agent browsing                           | [Playwright MCP](https://developers.cloudflare.com/browser-run/playwright/playwright-mcp/) or [CDP with MCP clients](https://developers.cloudflare.com/browser-run/cdp/mcp-clients/)                         | LLMs control browsers via MCP                                    |
| Resilient scraping                          | [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/)                                                                                                                                        | AI finds elements by intent, not selectors                       |
| Direct browser control from any environment | [CDP](https://developers.cloudflare.com/browser-run/cdp/)                                                                                                                                                    | WebSocket access from local machines, CI/CD, or external servers |

## Key features

* **Scale to thousands of browsers**: Instant access to a global pool of browsers with low cold-start time, ideal for high-volume screenshot generation, data extraction, or automation at scale
* **Global by default**: Browser sessions run on Cloudflare's edge network, opening close to your users for better speed and availability worldwide
* **Easy to integrate**: [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) for common tasks, [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) and [Playwright](https://developers.cloudflare.com/browser-run/playwright/) for complex workflows, and [CDP](https://developers.cloudflare.com/browser-run/cdp/) for direct browser control from any environment
* **Session management**: [Reuse browser sessions](https://developers.cloudflare.com/browser-run/features/reuse-sessions/) across requests to improve performance and reduce cold-start overhead
* **Flexible pricing**: Pay only for browser time used with generous free tier ([view pricing](https://developers.cloudflare.com/browser-run/pricing/))

## Related products

**[Workers](https://developers.cloudflare.com/workers/)** 

Build serverless applications and deploy instantly across the globe for exceptional performance, reliability, and scale.

**[Durable Objects](https://developers.cloudflare.com/durable-objects/)** 

A globally distributed coordination API with strongly consistent storage. Using Durable Objects to [persist browser sessions](https://developers.cloudflare.com/browser-run/how-to/browser-run-with-do/) improves performance by eliminating the time that it takes to spin up a new browser session.

**[Agents](https://developers.cloudflare.com/agents/)** 

Build AI-powered agents that autonomously navigate websites and perform tasks using [Playwright MCP](https://developers.cloudflare.com/browser-run/playwright/playwright-mcp/) or [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/).

## More resources

[Get started](https://developers.cloudflare.com/browser-run/get-started/) 

Choose an integration method and deploy your first project.

[Limits](https://developers.cloudflare.com/browser-run/limits/) 

Learn about Browser Run limits.

[Pricing](https://developers.cloudflare.com/browser-run/pricing/) 

Learn about Browser Run pricing.

[Playwright API](https://developers.cloudflare.com/browser-run/playwright/) 

Use Cloudflare's fork of Playwright for testing and automation.

[Developer Discord](https://discord.cloudflare.com) 

Connect with the Workers community on Discord to ask questions, show what you are building, and discuss the platform with other developers.

[@CloudflareDev](https://x.com/cloudflaredev) 

Follow @CloudflareDev on Twitter to learn about product announcements, and what is new in Cloudflare Workers.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}}]}
```

---

---
title: Get started
description: Cloudflare Browser Run (formerly Browser Rendering) allows you to programmatically control a headless browser, enabling you to do things like take screenshots, generate PDFs, and perform automated browser tasks. This guide will help you choose the right integration method and get you started with your first project.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/get-started.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Get started

Cloudflare Browser Run (formerly Browser Rendering) allows you to programmatically control a headless browser, enabling you to do things like take screenshots, generate PDFs, and perform automated browser tasks. This guide will help you choose the right integration method and get you started with your first project.

Browser Run offers two categories of integration methods:

* **[Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/)**: Simple, stateless browser tasks like screenshots, PDFs, and scraping. No code deployment needed.
* **Browser Sessions**: Direct browser control via [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), [CDP](https://developers.cloudflare.com/browser-run/cdp/), or [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/). Deploy within Cloudflare Workers or connect from any environment via CDP.

| Use case                                    | Recommended                                                                                                                                                                                                  | Why                                                              |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| Simple screenshot, PDF, or scrape           | [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/)                                                                                                                                | No code deployment; single HTTP request                          |
| Browser automation                          | [Playwright](https://developers.cloudflare.com/browser-run/playwright/), [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/) | Full browser control with scripting                              |
| Porting existing scripts                    | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/) | Minimal code changes from standard libraries                     |
| AI-powered data extraction                  | [JSON endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/)                                                                                                                  | Structured data via natural language prompts                     |
| Site-wide crawling                          | [Crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/)                                                                                                                | Multi-page content extraction with async results                 |
| AI agent browsing                           | [Playwright MCP](https://developers.cloudflare.com/browser-run/playwright/playwright-mcp/) or [CDP with MCP clients](https://developers.cloudflare.com/browser-run/cdp/mcp-clients/)                         | LLMs control browsers via MCP                                    |
| Resilient scraping                          | [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/)                                                                                                                                        | AI finds elements by intent, not selectors                       |
| Direct browser control from any environment | [CDP](https://developers.cloudflare.com/browser-run/cdp/)                                                                                                                                                    | WebSocket access from local machines, CI/CD, or external servers |

## Quick Actions

### Prerequisites

* Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
* Create a [Cloudflare API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permissions.

### Example: Take a screenshot of the Cloudflare homepage

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com"

  }' \

  --output "screenshot.png"


```

Other Quick Actions endpoints include:

* [Fetch HTML](https://developers.cloudflare.com/browser-run/quick-actions/content-endpoint/)
* [Generate a PDF](https://developers.cloudflare.com/browser-run/quick-actions/pdf-endpoint/)
* [Crawl web content](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/)

Check out the full list of [Quick Actions endpoints](https://developers.cloudflare.com/browser-run/quick-actions/).

## Browser Sessions

### Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

### Example: Navigate to a URL, take a screenshot, and store in KV

#### 1\. Create a Worker project

[Cloudflare Workers](https://developers.cloudflare.com/workers/) provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure. Your Worker application is a container to interact with a headless browser to do actions, such as taking screenshots.

Create a new Worker project named `browser-worker` by running:

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- browser-worker
```

```
yarn create cloudflare browser-worker
```

```
pnpm create cloudflare@latest browser-worker
```

For setup, select the following options:

* For _What would you like to start with?_, choose `Hello World example`.
* For _Which template would you like to use?_, choose `Worker only`.
* For _Which language do you want to use?_, choose `JavaScript / TypeScript`.
* For _Do you want to use git for version control?_, choose `Yes`.
* For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying).

#### 2\. Install Puppeteer

In your `browser-worker` directory, install Cloudflare’s [fork of Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/):

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/puppeteer
```

```
yarn add -D @cloudflare/puppeteer
```

```
pnpm add -D @cloudflare/puppeteer
```

```
bun add -d @cloudflare/puppeteer
```

#### 3\. Create a KV namespace

Browser Run can be used with other developer products. You might need a [relational database](https://developers.cloudflare.com/d1/), an [R2 bucket](https://developers.cloudflare.com/r2/) to archive your crawled pages and assets, a [Durable Object](https://developers.cloudflare.com/durable-objects/) to keep your browser instance alive and share it with multiple requests, or [Queues](https://developers.cloudflare.com/queues/) to handle your jobs asynchronously.

For the purpose of this example, we will use a [KV store](https://developers.cloudflare.com/kv/concepts/kv-namespaces/) to cache your screenshots.

Create two namespaces, one for production and one for development.

Terminal window

```

npx wrangler kv namespace create BROWSER_KV_DEMO

npx wrangler kv namespace create BROWSER_KV_DEMO --preview


```

Take note of the IDs for the next step.

#### 4\. Configure the Wrangler configuration file

Configure your `browser-worker` project's [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) by adding a browser [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) and a [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Bindings allow your Workers to interact with resources on the Cloudflare developer platform. Your browser `binding` name is set by you, this guide uses the name `MYBROWSER`. Browser bindings allow for communication between a Worker and a headless browser which allows you to do actions such as taking a screenshot, generating a PDF, and more.

Update your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) with the Browser Run API binding and the KV namespaces you created:

* [  wrangler.jsonc ](#tab-panel-3554)
* [  wrangler.toml ](#tab-panel-3555)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "browser-worker",

  "main": "src/index.js",

  // Set this to today's date

  "compatibility_date": "2026-04-16",

  "compatibility_flags": ["nodejs_compat"],

  "browser": {

    "binding": "MYBROWSER"

  },

  "kv_namespaces": [

    {

      "binding": "BROWSER_KV_DEMO",

      "id": "22cf855786094a88a6906f8edac425cd",

      "preview_id": "e1f8b68b68d24381b57071445f96e623"

    }

  ]

}


```

Explain Code

TOML

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "browser-worker"

main = "src/index.js"

# Set this to today's date

compatibility_date = "2026-04-16"

compatibility_flags = [ "nodejs_compat" ]


[browser]

binding = "MYBROWSER"


[[kv_namespaces]]

binding = "BROWSER_KV_DEMO"

id = "22cf855786094a88a6906f8edac425cd"

preview_id = "e1f8b68b68d24381b57071445f96e623"


```

Explain Code

#### 5\. Code

* [  JavaScript ](#tab-panel-3552)
* [  TypeScript ](#tab-panel-3553)

Update `src/index.js` with your Worker code:

JavaScript

```

import puppeteer from "@cloudflare/puppeteer";


export default {

  async fetch(request, env) {

    const { searchParams } = new URL(request.url);

    let url = searchParams.get("url");

    let img;

    if (url) {

      url = new URL(url).toString(); // normalize

      img = await env.BROWSER_KV_DEMO.get(url, { type: "arrayBuffer" });

      if (img === null) {

        const browser = await puppeteer.launch(env.MYBROWSER);

        const page = await browser.newPage();

        await page.goto(url);

        img = await page.screenshot();

        await env.BROWSER_KV_DEMO.put(url, img, {

          expirationTtl: 60 * 60 * 24,

        });

        await browser.close();

      }

      return new Response(img, {

        headers: {

          "content-type": "image/jpeg",

        },

      });

    } else {

      return new Response("Please add an ?url=https://example.com/ parameter");

    }

  },

};


```

Explain Code

Update `src/index.ts` with your Worker code:

TypeScript

```

import puppeteer from "@cloudflare/puppeteer";


interface Env {

  MYBROWSER: Fetcher;

  BROWSER_KV_DEMO: KVNamespace;

}


export default {

  async fetch(request, env): Promise<Response> {

    const { searchParams } = new URL(request.url);

    let url = searchParams.get("url");

    let img: Buffer;

    if (url) {

      url = new URL(url).toString(); // normalize

      img = await env.BROWSER_KV_DEMO.get(url, { type: "arrayBuffer" });

      if (img === null) {

        const browser = await puppeteer.launch(env.MYBROWSER);

        const page = await browser.newPage();

        await page.goto(url);

        img = (await page.screenshot()) as Buffer;

        await env.BROWSER_KV_DEMO.put(url, img, {

          expirationTtl: 60 * 60 * 24,

        });

        await browser.close();

      }

      return new Response(img, {

        headers: {

          "content-type": "image/jpeg",

        },

      });

    } else {

      return new Response("Please add an ?url=https://example.com/ parameter");

    }

  },

} satisfies ExportedHandler<Env>;


```

Explain Code

This Worker instantiates a browser using Puppeteer, opens a new page, navigates to the location of the 'url' parameter, takes a screenshot of the page, stores the screenshot in KV, closes the browser, and responds with the JPEG image of the screenshot.

If your Worker is running in production, it will store the screenshot to the production KV namespace. If you are running `wrangler dev`, it will store the screenshot to the dev KV namespace.

If the same `url` is requested again, it will use the cached version in KV instead, unless it expired.

#### 6\. Test

Run `npx wrangler dev` to test your Worker locally.

Use real headless browser during local development

To interact with a real headless browser during local development, set `"remote" : true` in the Browser binding configuration. Learn more in our [remote bindings documentation](https://developers.cloudflare.com/workers/development-testing/#remote-bindings).

To test taking your first screenshot, go to the following URL:

`<LOCAL_HOST_URL>/?url=https://example.com`

#### 7\. Deploy

Run `npx wrangler deploy` to deploy your Worker to the Cloudflare global network.

To take your first screenshot, go to the following URL:

```

<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/?url=https://example.com


```

## Next steps

* Check out all the [Quick Actions endpoints](https://developers.cloudflare.com/browser-run/quick-actions/)
* Try out the [Playwright MCP](https://developers.cloudflare.com/browser-run/playwright/playwright-mcp/)
* Connect from any environment using [CDP](https://developers.cloudflare.com/browser-run/cdp/)
* Learn more about Browser Run [limits](https://developers.cloudflare.com/browser-run/limits/) and [pricing](https://developers.cloudflare.com/browser-run/pricing/)

If you have any feature requests or notice any bugs, share your feedback directly with the Cloudflare team by joining the [Cloudflare Developers community on Discord ↗](https://discord.cloudflare.com/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/get-started/","name":"Get started"}}]}
```

---

---
title: Examples
description: Use these Quick Actions examples to perform common tasks with a single HTTP request.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/examples.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Examples

## Quick Actions examples

Use these [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) examples to perform common tasks with a single HTTP request.

[ Fetch rendered HTML from a URL ](https://developers.cloudflare.com/browser-run/quick-actions/content-endpoint/#fetch-rendered-html-from-a-url) Capture fully rendered HTML from a webpage after JavaScript execution. 

[ Take a screenshot of the visible viewport ](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/#basic-usage) Capture a screenshot of a fully rendered webpage from a URL or custom HTML. 

[ Take a screenshot of the full page ](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/#navigate-and-capture-a-full-page-screenshot) Capture a screenshot of an entire scrollable webpage, not just the visible viewport. 

[ Take a screenshot of an authenticated page ](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/#capture-a-screenshot-of-an-authenticated-page) Capture a screenshot of a webpage that requires login using cookies, HTTP Basic Auth, or custom headers. 

[ Generate a PDF ](https://developers.cloudflare.com/browser-run/quick-actions/pdf-endpoint/#basic-usage) Generate a PDF from a URL or custom HTML and CSS. 

[ Extract Markdown from a URL ](https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/#convert-a-url-to-markdown) Convert a webpage's content into Markdown format. 

[ Capture a snapshot from a URL ](https://developers.cloudflare.com/browser-run/quick-actions/snapshot/#capture-a-snapshot-from-a-url) Capture both the rendered HTML and a screenshot from a webpage in a single request. 

[ Scrape headings and links from a URL ](https://developers.cloudflare.com/browser-run/quick-actions/scrape-endpoint/#extract-headings-and-links-from-a-url) Extract structured data from specific elements on a webpage using CSS selectors. 

[ Capture structured data with an AI prompt and JSON schema ](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/#with-a-prompt-and-json-schema) Extract structured data from a webpage using AI using a prompt or JSON schema. 

[ Retrieve links from a URL ](https://developers.cloudflare.com/browser-run/quick-actions/links-endpoint/#get-all-links-on-a-page) Retrieve all links from a webpage, including hidden ones. 

[ Crawl a documentation site ](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/#documentation-site-crawl) Crawl documentation pages with include/exclude patterns to build a knowledge base. 

[ Extract structured product data with AI ](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/#product-catalog-extraction-with-ai) Crawl a product catalog and extract structured JSON data using AI. 

[ Fast static content fetch ](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/#fast-static-content-fetch) Crawl static sites without JavaScript rendering for faster results. 

## Browser automation examples

Use [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), or [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/) for dynamic, multi-step browser automation within Cloudflare Workers.

[ Get page metrics with Puppeteer ](https://developers.cloudflare.com/browser-run/puppeteer/#use-puppeteer-in-a-worker) Use Puppeteer to navigate to a page and retrieve performance metrics in a Worker. 

[ Take a screenshot with Playwright ](https://developers.cloudflare.com/browser-run/playwright/#take-a-screenshot) Use Playwright to navigate to a page, interact with elements, and capture a screenshot. 

[ Run test assertions with Playwright ](https://developers.cloudflare.com/browser-run/playwright/#assertions) Use Playwright assertions to test web applications in a Worker. 

[ Generate a trace with Playwright ](https://developers.cloudflare.com/browser-run/playwright/#trace) Capture detailed execution logs for debugging with Playwright tracing. 

[ Reuse browser sessions ](https://developers.cloudflare.com/browser-run/features/reuse-sessions/) Improve performance by reusing browser sessions across requests. 

[ Persist sessions with Durable Objects ](https://developers.cloudflare.com/browser-run/how-to/browser-run-with-do/) Use Durable Objects to maintain long-running browser sessions. 

[ AI-powered browser automation with Stagehand ](https://developers.cloudflare.com/browser-run/stagehand/#use-stagehand-in-a-worker-with-workers-ai) Use natural language instructions to automate browser tasks with AI. 

## CDP examples

Use [CDP](https://developers.cloudflare.com/browser-run/cdp/) to connect to Browser Run from any environment using the Chrome DevTools Protocol.

[ Connect Puppeteer from your local machine ](https://developers.cloudflare.com/browser-run/cdp/puppeteer/) Run Puppeteer scripts against Browser Run from Node.js on your local machine, CI/CD, or any external server. 

[ Configure AI agents with MCP ](https://developers.cloudflare.com/browser-run/cdp/mcp-clients/) Set up Claude Desktop, Claude Code, Cursor, or other MCP clients to control browsers via Browser Run. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/examples/","name":"Examples"}}]}
```

---

---
title: Quick Actions
description: Quick Actions provide simple HTTP endpoints for common browser tasks like capturing screenshots, extracting HTML content, generating PDFs, and more.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Quick Actions

Quick Actions provide simple HTTP endpoints for common browser tasks like capturing screenshots, extracting HTML content, generating PDFs, and more.

The following are the available options:

* [ /content - Fetch HTML ](https://developers.cloudflare.com/browser-run/quick-actions/content-endpoint/)
* [ /screenshot - Capture screenshot ](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/)
* [ /pdf - Render PDF ](https://developers.cloudflare.com/browser-run/quick-actions/pdf-endpoint/)
* [ /markdown - Extract Markdown from a webpage ](https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/)
* [ /snapshot - Take a webpage snapshot ](https://developers.cloudflare.com/browser-run/quick-actions/snapshot/)
* [ /scrape - Scrape HTML elements ](https://developers.cloudflare.com/browser-run/quick-actions/scrape-endpoint/)
* [ /json - Capture structured data using AI ](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/)
* [ /links - Retrieve links from a webpage ](https://developers.cloudflare.com/browser-run/quick-actions/links-endpoint/)
* [ /crawl - Crawl web content ](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/)
* [ Reference ](https://developers.cloudflare.com/api/resources/browser%5Frendering/)

Use Quick Actions when you need a fast, simple way to perform common browser tasks such as capturing screenshots, extracting HTML, or generating PDFs without writing complex scripts. For more advanced automation, custom workflows, or persistent browser sessions, use [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/).

## Before you begin

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with the following permissions:

* `Browser Rendering - Edit`

Note

You can monitor Browser Run (formerly Browser Rendering) usage in two ways:

* In the Cloudflare dashboard, go to the **Browser Run** page to view aggregate metrics, including total Quick Actions requests and total browser hours used.[ Go to **Browser Run** ](https://dash.cloudflare.com/?to=/:account/workers/browser-run)
* `X-Browser-Ms-Used` header: Returned in every Quick Actions response, reporting browser time used for that request (in milliseconds).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}}]}
```

---

---
title: Reference
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/api-reference.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Reference

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/api-reference/","name":"Reference"}}]}
```

---

---
title: /content - Fetch HTML
description: The /content endpoint instructs the browser to navigate to a website and capture the fully rendered HTML of a page, including the head section, after JavaScript execution. This is ideal for capturing content from JavaScript-heavy or interactive websites.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/content-endpoint.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /content - Fetch HTML

The `/content` endpoint instructs the browser to navigate to a website and capture the fully rendered HTML of a page, including the `head` section, after JavaScript execution. This is ideal for capturing content from JavaScript-heavy or interactive websites.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/content


```

## Required fields

You must provide either `url` or `html`:

* `url` (string)
* `html` (string)

## Common use cases

* Capture the fully rendered HTML of a dynamic page
* Extract HTML for parsing, scraping, or downstream processing

## Basic usage

### Fetch rendered HTML from a URL

* [ curl ](#tab-panel-3580)
* [ TypeScript SDK ](#tab-panel-3581)

Go to `https://developers.cloudflare.com/` and return the rendered HTML.

Terminal window

```

curl -X 'POST' 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/content' \

  -H 'Content-Type: application/json' \

  -H 'Authorization: Bearer <apiToken>' \

  -d '{"url": "https://developers.cloudflare.com/"}'


```

TypeScript

```

import Cloudflare from "cloudflare";


const client = new Cloudflare({

  apiToken: process.env["CLOUDFLARE_API_TOKEN"],

});


const content = await client.browserRendering.content.create({

  account_id: process.env["CLOUDFLARE_ACCOUNT_ID"],

  url: "https://developers.cloudflare.com/",

});


console.log(content);


```

Explain Code

## Advanced usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/content/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Block specific resource types

Navigate to `https://cloudflare.com/` but block images and stylesheets from loading. Undesired requests can be blocked by resource type (`rejectResourceTypes`) or by using a regex pattern (`rejectRequestPattern`). The opposite can also be done, only allow requests that match `allowRequestPattern` or `allowResourceTypes`.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/content' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

      "url": "https://cloudflare.com/",

      "rejectResourceTypes": ["image"],

      "rejectRequestPattern": ["/^.*\\.(css)"]

    }'


```

Many more options exist, like setting HTTP headers using `setExtraHTTPHeaders`, setting `cookies`, and using `gotoOptions` to control page load behaviour - check the endpoint [reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/content/methods/create/) for all available parameters.

### Handling JavaScript-heavy pages

For JavaScript-heavy pages or Single Page Applications (SPAs), the default page load behavior may return empty or incomplete results. This happens because the browser considers the page loaded before JavaScript has finished rendering the content.

The simplest solution is to use the `gotoOptions.waitUntil` parameter set to `networkidle0` or `networkidle2`:

```

{

  "url": "https://example.com",

  "gotoOptions": {

    "waitUntil": "networkidle0"

  }

}


```

For faster responses, advanced users can use `waitForSelector` to wait for a specific element instead of waiting for all network activity to stop. This requires knowing which CSS selector indicates the content you need has loaded. For more details, refer to [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Set a custom user agent

You can change the user agent at the page level by passing `userAgent` as a top-level parameter in the JSON body. This is useful if the target website serves different content based on the user agent.

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Run requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#non-configurable-headers) rather than relying on the User-Agent string.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/content-endpoint/","name":"/content - Fetch HTML"}}]}
```

---

---
title: /crawl - Crawl web content
description: The /crawl endpoint scrapes content from a starting URL and follows links across the site, up to a configurable depth or page limit. Responses can be returned as HTML, Markdown, or JSON.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/crawl-endpoint.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /crawl - Crawl web content

The `/crawl` endpoint scrapes content from a starting URL and follows links across the site, up to a configurable depth or page limit. Responses can be returned as HTML, Markdown, or JSON.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<account_id>/browser-rendering/crawl


```

## Required fields

* `url` (string)

Refer to [optional parameters](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/#optional-parameters) for additional customization options.

## Common use cases

* Building knowledge bases or training AI systems (such as [RAG applications](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-rag/)) with up-to-date web content
* Scraping and analyzing content across multiple pages for research, summarization, or monitoring

## How it works

There are two steps to using the `/crawl` endpoint:

1. [Initiate the crawl job](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/#initiate-the-crawl-job) — A `POST` request where you initiate the crawl and receive a response with a job `id`.
2. [Request results of the crawl job](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/#request-results-of-the-crawl-job) — A `GET` request where you request the status or results of the crawl.

Crawl jobs have a maximum run time of seven days. If a job does not finish within this time, it will be cancelled due to timeout. Job results are available for 14 days after the job completes, after which the job data is deleted.

Free plan limitations

Users on the Workers Free plan are subject to additional crawl-specific restrictions. Refer to [crawl endpoint limits](https://developers.cloudflare.com/browser-run/limits/#crawl-endpoint-limits) for details.

## Initiate the crawl job

Send a `POST` request with a `url` to start a crawl job. The API responds immediately with a job `id` you will use to retrieve results. Refer to [optional parameters](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/#optional-parameters) for additional customization options.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://developers.cloudflare.com/workers/"

  }'


```

Example response:

```

{

  "success": true,

  "result": "c7f8s2d9-a8e7-4b6e-8e4d-3d4a1b2c3f4e"

}


```

## Request results of the crawl job

To check the status or request the results of your crawl job, use the job `id` you received:

Terminal window

```

curl -X GET 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl/c7f8s2d9-a8e7-4b6e-8e4d-3d4a1b2c3f4e' \

  -H 'Authorization: Bearer YOUR_API_TOKEN'


```

The response includes a `status` field indicating the current state of the crawl job. The possible job statuses are:

* `running` — The crawl job is currently in progress.
* `cancelled_due_to_timeout` — The crawl job exceeded the maximum run time of seven days.
* `cancelled_due_to_limits` — The crawl job was cancelled because it hit [account limits](https://developers.cloudflare.com/browser-run/limits/).
* `cancelled_by_user` — The crawl job was manually cancelled by the user.
* `errored` — The crawl job encountered an error.
* `completed` — The crawl job finished successfully.

### Polling for completion

Since crawl jobs run asynchronously, you can poll the endpoint periodically to check when the job finishes. Add `?limit=1` to the request URL so the response stays lightweight — you only need the job `status`, not the full set of crawled records.

JavaScript

```

async function waitForCrawl(accountId, jobId, apiToken) {

  const maxAttempts = 60;

  const delayMs = 5000;


  for (let i = 0; i < maxAttempts; i++) {

    const response = await fetch(

      `https://api.cloudflare.com/client/v4/accounts/${accountId}/browser-rendering/crawl/${jobId}?limit=1`,

      {

        headers: {

          Authorization: `Bearer ${apiToken}`,

        },

      },

    );


    const data = await response.json();

    const status = data.result.status;


    if (status !== "running") {

      return data.result;

    }


    await new Promise((resolve) => setTimeout(resolve, delayMs));

  }


  throw new Error("Crawl job did not complete within timeout");

}


```

Explain Code

Once the job reaches a terminal status, fetch the full results without the `limit` parameter. You can also use the following query parameters to filter and paginate results:

* `cursor` — Cursor for pagination. If the response exceeds 10 MB, a `cursor` value will be included. Pass it as a query parameter to retrieve the next page of results.
* `limit` — Maximum number of records to return.
* `status` — Filter by URL status: `queued`, `completed`, `disallowed`, `skipped`, `errored`, or `cancelled`.

Example with query parameters:

Terminal window

```

curl -X GET 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl/c7f8s2d9-a8e7-4b6e-8e4d-3d4a1b2c3f4e?cursor=10&limit=10&status=completed' \

  -H 'Authorization: Bearer YOUR_API_TOKEN'


```

Example response:

```

{

  "result": {

    "id": "c7f8s2d9-a8e7-4b6e-8e4d-3d4a1b2c3f4e",

    "status": "completed",

    "browserSecondsUsed": 134.7,

    "total": 50,

    "finished": 50,

    "records": [

      {

        "url": "https://developers.cloudflare.com/workers/",

        "status": "completed",

        "markdown": "# Cloudflare Workers\nBuild and deploy serverless applications...",

        "metadata": {

          "status": 200,

          "title": "Cloudflare Workers · Cloudflare Workers docs",

          "url": "https://developers.cloudflare.com/workers/"

        }

      },

      {

        "url": "https://developers.cloudflare.com/workers/get-started/quickstarts/",

        "status": "completed",

        "markdown": "## Quickstarts\nGet up and running with a simple Hello World...",

        "metadata": {

          "status": 200,

          "title": "Quickstarts · Cloudflare Workers docs",

          "url": "https://developers.cloudflare.com/workers/get-started/quickstarts/"

        }

      }

      // ... 48 more entries omitted for brevity

    ],

    "cursor": 10

  },

  "success": true

}


```

Explain Code

### Errored and blocked pages

If a crawled page returns an HTTP error (such as `402`, `403`, or `500`), the record for that URL will have `"status": "errored"`.

This information is only available in the crawl results (step 2) — the [initiation response](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/#initiate-the-crawl-job) only returns the job `id`. Because crawl jobs run asynchronously, the crawler does not fetch page content at initiation time.

To view only errored records, filter by `status=errored`:

Terminal window

```

curl -X GET 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl/{job_id}?status=errored' \

  -H 'Authorization: Bearer YOUR_API_TOKEN'


```

The record's `status` field contains the HTTP status code returned by the origin server, and `html` contains the response body. This is useful for understanding site owners' intent when they block crawlers — for example, sites using [AI Crawl Control ↗](https://blog.cloudflare.com/ai-crawl-control) may return a custom status code and message.

## Cancel a crawl job

To cancel a crawl job that is currently in progress, use the job `id` you received:

Terminal window

```

curl -X DELETE 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl/c7f8s2d9-a8e7-4b6e-8e4d-3d4a1b2c3f4e' \

  -H 'Authorization: Bearer YOUR_API_TOKEN'


```

A successful cancellation will return a `200 OK` status code. The job status will be updated to cancelled, and all URLs that have been queued to be crawled will be cancelled.

## Optional parameters

The following optional parameters can be used in your crawl request, in addition to the required `url` parameter. These are parameters specific to the `/crawl` endpoint.

When `render` is `true` (the default), crawl jobs also support all standard Browser Run parameters such as `rejectResourceTypes`, `rejectRequestPattern`, `cookies`, and `setExtraHTTPHeaders`. When `render` is `false`, only the crawl-specific parameters listed in the table below are supported. For the full list, refer to the [API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/crawl/methods/create/).

| Optional parameter           | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| limit                        | Number           | Maximum number of pages to crawl (default is 10, maximum is 100,000).                                                                                                                                                                                                                                                                                                                                                                       |
| depth                        | Number           | Maximum link depth to crawl from the starting URL (default is 100,000, maximum is 100,000).                                                                                                                                                                                                                                                                                                                                                 |
| source                       | String           | Source for discovering URLs. Options are all, sitemaps, or links. Default is all.                                                                                                                                                                                                                                                                                                                                                           |
| formats                      | Array of strings | Response format (default is HTML, other options are Markdown and JSON). The JSON format leverages [Workers AI](https://developers.cloudflare.com/workers-ai/) by default for data extraction, which incurs usage on Workers AI. Refer to the [/json endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/) to learn more, including how to use a custom model and fallbacks.                                 |
| render                       | Boolean          | If false, does a fast HTML fetch without executing JavaScript (default is true, [learn more about render](#render-parameter)).                                                                                                                                                                                                                                                                                                              |
| jsonOptions                  | Object           | Only required if formats includes json. Contains prompt, response\_format, and custom\_ai properties (same types as the [/json endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/)).                                                                                                                                                                                                                      |
| maxAge                       | Number           | Maximum length of time in seconds the crawler can use a cached resource before it must re-fetch it from the origin server (default is 86,400, maximum is 604,800). Cache is served from R2 only if the URL and parameters exactly match.                                                                                                                                                                                                    |
| modifiedSince                | Number           | Unix timestamp (in seconds) indicating to only crawl pages that were modified since this time.                                                                                                                                                                                                                                                                                                                                              |
| options.includeExternalLinks | Boolean          | If true, follows links to external domains (default is false).                                                                                                                                                                                                                                                                                                                                                                              |
| options.includeSubdomains    | Boolean          | If true, follows links to subdomains of the starting URL (default is false).                                                                                                                                                                                                                                                                                                                                                                |
| options.includePatterns      | Array of strings | Only visits URLs that match one of these wildcard patterns. Use \* to match any characters except /, or \*\* to match any characters including /.                                                                                                                                                                                                                                                                                           |
| options.excludePatterns      | Array of strings | Does not visit URLs that match any of these wildcard patterns. Use \* to match any characters except /, or \*\* to match any characters including /.                                                                                                                                                                                                                                                                                        |
| crawlPurposes                | Array of strings | Declares the intended use of crawled content for [Content Signals ↗](https://contentsignals.org/) enforcement. Allowed values: search, ai-input, ai-train. Default is \["search", "ai-input", "ai-train"\]. If a target site's robots.txt includes a Content-Signal directive that sets any of your declared purposes to no, the crawl request will be rejected with a 400 error. Refer to [Content Signals](#content-signals) for details. |

### Pattern behavior

`excludePatterns` has strictly higher priority. If a URL matches an exclude rule, it is skipped, regardless of whether it matches an include rule.

* **No rules** — Everything is indexed.
* **Exclude only** — Everything is indexed except items matching the exclude patterns.
* **Include only** — Only items matching the include patterns are indexed; everything else is ignored.

### Viewing skipped URLs

To view URLs that were discovered but skipped, query the crawl job results with `status=skipped`. URLs can be skipped due to `includeExternalLinks`, `includeSubdomains`, `includePatterns`/`excludePatterns`, or the `modifiedSince` parameter. Skipped URLs will also be visible in the dashboard in a future release.

Terminal window

```

curl -X GET 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl/{job_id}?status=skipped' \

  -H 'Authorization: Bearer YOUR_API_TOKEN'


```

### `render` parameter

If you use `render: true`, which is the default, the `crawl` endpoint spins up a headless browser and executes page JavaScript. If you use `render: false`, the `crawl` endpoint does a fast HTML fetch without executing JavaScript.

Use `render: true` when the page builds content in the browser. Use `render: false` when the content you need is already in the initial HTML response.

Crawls that use `render: true` use a headless browser and are billed under typical Browser Run pricing. Crawls that use `render: false` run on [Workers](https://developers.cloudflare.com/workers/) instead of a headless browser. During the beta, `render: false` crawls are not billed. After the beta, they will be billed under [Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/).

### Example with all optional parameters

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://www.exampledocs.com/docs/",

    "crawlPurposes": ["search"],

    "limit": 50,

    "depth": 2,

    "formats": ["markdown"],

    "render": false,

    "maxAge": 7200,

    "modifiedSince": 1704067200,

    "source": "all",

    "options": {

      "includeExternalLinks": true,

      "includeSubdomains": true,

      "includePatterns": [

        "**/api/v1/*"

      ],

      "excludePatterns": [

        "*/learning-paths/*"

      ]

    }

}'


```

Explain Code

## Advanced usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/crawl/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Documentation site crawl

Crawl only documentation pages and exclude specific sections:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/docs",

    "limit": 200,

    "depth": 5,

    "formats": ["markdown"],

    "options": {

      "includePatterns": [

        "https://example.com/docs/**"

      ],

      "excludePatterns": [

        "https://example.com/docs/changelog/**",

        "https://example.com/docs/archive/**"

      ]

    }

  }'


```

Explain Code

### Product catalog extraction with AI

Extract structured product data using the `json` format. This leverages [Workers AI](https://developers.cloudflare.com/workers-ai/) by default. Refer to the [/json endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/) to learn more.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://shop.example.com/products",

    "limit": 50,

    "formats": ["json"],

    "jsonOptions": {

      "prompt": "Extract product name, price, description, and availability",

      "response_format": {

        "type": "json_schema",

        "json_schema": {

          "name": "product",

          "properties": {

            "name": "string",

            "price": "number",

            "currency": "string",

            "description": "string",

            "inStock": "boolean"

          }

        }

      }

    },

    "options": {

      "includePatterns": [

        "https://shop.example.com/products/*"

      ]

    }

  }'


```

Explain Code

### Fast static content fetch

Fetch static HTML without rendering for faster crawling of static sites:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com",

    "limit": 100,

    "render": false,

    "formats": ["html", "markdown"]

  }'


```

### Crawl with authentication

Crawl pages behind HTTP authentication or with custom headers:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://secure.example.com",

    "limit": 50,

    "authenticate": {

      "username": "user",

      "password": "pass"

    }

  }'


```

Explain Code

You can also use cookies or custom headers for token-based authentication:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://api.example.com/docs",

    "limit": 100,

    "setExtraHTTPHeaders": {

      "X-API-Key": "your-api-key"

    }

  }'


```

Explain Code

### Wait for dynamic content

Crawl single-page applications that load content dynamically:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://app.example.com",

    "limit": 50,

    "gotoOptions": {

      "waitUntil": "networkidle2",

      "timeout": 60000

    },

    "waitForSelector": {

      "selector": "[data-content-loaded]",

      "timeout": 30000,

      "visible": true

    }

  }'


```

Explain Code

### Block unnecessary resources

Speed up crawling by blocking images and media. `rejectResourceTypes` is only available when `render` is `true` (the default).

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com",

    "limit": 100,

    "rejectResourceTypes": [

      "image",

      "media",

      "font",

      "stylesheet"

    ]

  }'


```

Explain Code

## Crawler behavior

### How the crawler discovers URLs

The crawler discovers and processes URLs in the following order (when using `source: all`, the default):

1. **Starting URL** — The URL specified in your request.
2. **Sitemap links** — URLs found in the site's sitemap.
3. **Page links** — Links scraped from pages, if not already found in the sitemap.

Use the `source` parameter to customize which sources the crawler uses. The available options are:

* `all` — Uses both sitemaps and page links (default).
* `sitemaps` — Only crawls URLs found in the site's sitemap.
* `links` — Only crawls links found on pages, ignoring sitemaps.

### robots.txt and bot protection

The `/crawl` endpoint respects the directives of `robots.txt` files, including `crawl-delay`. If a site does not specify a `crawl-delay` in its `robots.txt`, the crawler uses a default delay of 0.5 seconds between requests to the same domain to avoid overwhelming the origin server. All URLs that `/crawl` is directed not to crawl are listed in the response with `"status": "disallowed"`. For guidance on configuring `robots.txt` and sitemaps for sites you plan to crawl, refer to [robots.txt and sitemaps](https://developers.cloudflare.com/browser-run/reference/robots-txt/). If you want to block the `/crawl` endpoint from accessing your site, refer to [Blocking crawlers with robots.txt](https://developers.cloudflare.com/browser-run/reference/robots-txt/#blocking-crawlers-with-robotstxt).

Bot protection may block crawling

Browser Run does not bypass CAPTCHAs, Turnstile challenges, or any other bot protection mechanisms. If a target site uses Cloudflare products that control or restrict bot traffic such as [Bot Management](https://developers.cloudflare.com/bots/), [Web Application Firewall (WAF)](https://developers.cloudflare.com/waf/), or [Turnstile](https://developers.cloudflare.com/turnstile/), the same rules will apply to the Browser Run crawler.

If you are crawling your own site and want Browser Run to access it freely, you can create a WAF skip rule to allowlist Browser Run. Refer to [Can I allowlist Browser Run on my own website?](https://developers.cloudflare.com/browser-run/faq/#can-i-allowlist-browser-run-on-my-own-website) for instructions. The `/crawl` endpoint uses [bot detection ID](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#bot-detection) `128292352`.

### User-Agent

The `/crawl` endpoint uses `CloudflareBrowserRenderingCrawler/1.0` as its User-Agent, which is different from other [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) endpoints. This User-Agent is not customizable. Unlike other Quick Actions endpoints, the `userAgent` parameter is not supported on the `/crawl` endpoint.

For a full list of default User-Agent strings, refer to [Automatic request headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#user-agent).

### Content Signals

The `/crawl` endpoint respects [Content Signals ↗](https://contentsignals.org/) directives found in a target site's `robots.txt` file. Content Signals are a way for site owners to express preferences about how their content can be used by automated systems. For more background, refer to [Giving users choice with Cloudflare's new Content Signals Policy ↗](https://blog.cloudflare.com/content-signals-policy/).

A site owner can include a `Content-Signal` directive in their `robots.txt` to allow or disallow specific categories of use:

* `search` — Building a search index and providing search results with links and excerpts.
* `ai-input` — Inputting content into AI models at query time (for example, retrieval-augmented generation or grounding).
* `ai-train` — Training or fine-tuning AI models.

For example, a `robots.txt` that allows search indexing but disallows AI training:

robots.txt

```

User-Agent: *

Content-Signal: search=yes, ai-train=no

Allow: /


```

#### How /crawl enforces Content Signals

By default, `/crawl` declares all three purposes: `["search", "ai-input", "ai-train"]`. If a target site sets any of those content signals to `no`, the crawl request will be rejected at initiation with a `400 Bad Request` error unless you explicitly narrow your declared purposes using the `crawlPurposes` parameter to exclude the disallowed use.

This means:

1. **Site has no Content Signals** — The crawl proceeds normally.
2. **Site has Content Signals, and all your declared purposes are allowed** — The crawl proceeds normally.
3. **Site sets a content signal to `no`, and that purpose is in your `crawlPurposes`** — The crawl request is rejected with a `400` error and the message `Crawl purpose(s) completely disallowed by Content-Signal directive`.

To crawl a site that disallows AI training but allows search, set `crawlPurposes` to only the purposes you need:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/crawl' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com",

    "crawlPurposes": ["search"],

    "formats": ["markdown"]

  }'


```

In this example, because the operator declared only `search` as their purpose, the crawl will succeed even if the site sets `ai-train=no`.

Note

Content Signals are trust-based. By setting `crawlPurposes`, you are declaring to the site owner how you intend to use the crawled content.

## Troubleshooting

### Crawl job returns no results or all URLs are skipped

If your crawl job completes but returns an empty records array, or all URLs show `skipped` or `disallowed` status:

* **robots.txt blocking** — The crawler respects `robots.txt` rules. The `/crawl` endpoint identifies itself as `CloudflareBrowserRenderingCrawler/1.0`. Check the target site's `robots.txt` file to verify this user agent is allowed. Blocked URLs appear with `"status": "disallowed"`.
* **Pattern filters too restrictive** — Your `includePatterns` may not match any URLs on the site. Try crawling without patterns first to confirm URLs are discoverable, then add patterns.
* **No links found** — The starting URL may not contain links. Try using `source: "sitemaps"`, increasing the `depth` parameter, or setting `includeSubdomains` or `includeExternalLinks` to `true`.

### Crawl rejected by Content Signals

If your crawl request returns a `400 Bad Request` with the message `Crawl purpose(s) completely disallowed by Content-Signal directive`, the target site's `robots.txt` includes a `Content-Signal` directive that disallows one or more of your declared `crawlPurposes`. To resolve this, check the site's `robots.txt` for `Content-Signal:` entries and set `crawlPurposes` to only the purposes you need. For example, if the site sets `ai-train=no` and you only need search indexing, use `"crawlPurposes": ["search"]`. Refer to [Content Signals](#content-signals) for details.

### Crawl job takes too long

If a crawl job remains in `running` status for an extended period:

* **Slow page loads** — Pages with heavy JavaScript take longer to render. Use `render: false` if the content you need is in the initial HTML.
* **Rate limiting** — The crawler enforces a per-domain rate limit to avoid overwhelming origin servers. If a site specifies a `crawl-delay` in its `robots.txt`, the crawler respects it. Otherwise, the crawler uses a default delay of 0.5 seconds between requests to the same domain. If you run multiple crawl jobs targeting the same domain, they share the same per-domain rate limit, which can cause all jobs to take longer than if each ran individually.
* **Unnecessary resources** — Block resources that are not needed for content extraction using `rejectResourceTypes` (for example, `image`, `media`, `font`).

### Crawl job cancelled due to limits

A `cancelled_due_to_limits` status means your account hit its browser time limit. [Workers Free plan](https://developers.cloudflare.com/browser-run/limits/#workers-free) accounts are capped at 10 minutes of browser use per day. To resolve this:

* [Upgrade to a Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/) for higher [limits](https://developers.cloudflare.com/browser-run/limits/#workers-paid).
* Use `render: false` for static content to avoid consuming browser time.
* Increase `maxAge` to use cached results where possible.
* Reduce the `limit` parameter.

### JSON extraction errors

If the `json` format returns null or empty results:

* **Provide a clear prompt** — Be specific about what data to extract and where it appears on the page (for example, "Extract the product name, price, and description from the main product section").
* **Define a response schema** — Use `response_format` with a JSON schema to enforce the expected output structure.
* **Use a custom model** — If the default [Workers AI](https://developers.cloudflare.com/workers-ai/) model does not produce the desired results, use the `custom_ai` parameter to specify a different model. Refer to [Using a custom model (BYO API Key)](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/#using-a-custom-model-byo-api-key) for details.

If you have questions or encounter other errors, refer to the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/crawl-endpoint/","name":"/crawl - Crawl web content"}}]}
```

---

---
title: /json - Capture structured data using AI
description: The /json endpoint extracts structured data from a webpage. You can specify the expected output using either a prompt or a response_format parameter which accepts a JSON schema. The endpoint returns the extracted data in JSON format.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ JSON ](https://developers.cloudflare.com/search/?tags=JSON) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/json-endpoint.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /json - Capture structured data using AI

The `/json` endpoint extracts structured data from a webpage. You can specify the expected output using either a `prompt` or a `response_format` parameter which accepts a JSON schema. The endpoint returns the extracted data in JSON format.

Note

By default, the `/json` endpoint leverages [Workers AI](https://developers.cloudflare.com/workers-ai/) for data extraction using [@cf/meta/llama-3.3-70b-instruct-fp8-fast](https://developers.cloudflare.com/workers-ai/models/llama-3.3-70b-instruct-fp8-fast/). Using this endpoint incurs usage on Workers AI, which you can monitor in the [Workers AI Dashboard ↗](https://dash.cloudflare.com/?to=/:account/ai/workers-ai). To use a different model, refer to [Using a custom model (BYO API Key)](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/#using-a-custom-model-byo-api-key).

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/json


```

## Required fields

You must provide either `url` or `html`:

* `url` (string)
* `html` (string)

And at least one of:

* `prompt` (string), or
* `response_format` (object with a JSON Schema)

## Common use cases

* Extract product info (title, price, availability) or listings (jobs, rentals)
* Normalize article metadata (title, author, publish date, canonical URL)
* Convert unstructured pages into typed JSON for downstream pipelines

## Basic Usage

### With a Prompt and JSON schema

* [ curl ](#tab-panel-3582)
* [ TypeScript SDK ](#tab-panel-3583)

This example captures webpage data by providing both a prompt and a JSON schema. The prompt guides the extraction process, while the JSON schema defines the expected structure of the output.

Terminal window

```

curl --request POST 'https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT_ID/browser-rendering/json' \

  --header 'authorization: Bearer CF_API_TOKEN' \

  --header 'content-type: application/json' \

  --data '{

  "url": "https://developers.cloudflare.com/",

  "prompt": "Get me the list of AI products",

  "response_format": {

    "type": "json_schema",

    "schema": {

        "type": "object",

        "properties": {

          "products": {

            "type": "array",

            "items": {

              "type": "object",

              "properties": {

                "name": {

                  "type": "string"

                },

                "link": {

                  "type": "string"

                }

              },

              "required": [

                "name"

              ]

            }

          }

        }

      }

  }

}'


```

Explain Code

```

{

  "success": true,

  "result": {

    "products": [

      {

        "name": "Build a RAG app",

        "link": "https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/"

      },

      {

        "name": "Workers AI",

        "link": "https://developers.cloudflare.com/workers-ai/"

      },

      {

        "name": "Vectorize",

13 collapsed lines

        "link": "https://developers.cloudflare.com/vectorize/"

      },

      {

        "name": "AI Gateway",

        "link": "https://developers.cloudflare.com/ai-gateway/"

      },

      {

        "name": "AI Playground",

        "link": "https://playground.ai.cloudflare.com/"

      }

    ]

  }

}


```

Explain Code

### With only a prompt

In this example, only a prompt is provided. The endpoint will use the prompt to extract the data, but the response will not be structured according to a JSON schema. This is useful for simple extractions where you do not need a specific format.

Terminal window

```

curl --request POST 'https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT_ID/browser-rendering/json' \

  --header 'authorization: Bearer CF_API_TOKEN' \

  --header 'content-type: application/json' \

  --data '{

    "url": "https://developers.cloudflare.com/",

    "prompt": "get me the list of AI products"

  }'


```

```

  "success": true,

  "result": {

    "AI Products": [

      "Build a RAG app",

      "Workers AI",

      "Vectorize",

      "AI Gateway",

      "AI Playground"

    ]

  }

}


```

Explain Code

### With only a JSON schema (no prompt)

In this case, you supply a JSON schema via the `response_format` parameter. The schema defines the structure of the extracted data.

Terminal window

```

curl --request POST 'https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT_ID/browser-rendering/json' \

  --header 'authorization: Bearer CF_API_TOKEN' \

  --header 'content-type: application/json' \

  --data '"response_format": {

    "type": "json_schema",

    "schema": {

        "type": "object",

        "properties": {

          "products": {

            "type": "array",

            "items": {

              "type": "object",

              "properties": {

                "name": {

                  "type": "string"

                },

                "link": {

                  "type": "string"

                }

              },

              "required": [

                "name"

              ]

            }

          }

        }

      }

  }'


```

Explain Code

```

{

  "success": true,

  "result": {

    "products": [

      {

        "name": "Workers",

        "link": "https://developers.cloudflare.com/workers/"

      },

      {

        "name": "Pages",

        "link": "https://developers.cloudflare.com/pages/"

      },

55 collapsed lines

      {

        "name": "R2",

        "link": "https://developers.cloudflare.com/r2/"

      },

      {

        "name": "Images",

        "link": "https://developers.cloudflare.com/images/"

      },

      {

        "name": "Stream",

        "link": "https://developers.cloudflare.com/stream/"

      },

      {

        "name": "Build a RAG app",

        "link": "https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/"

      },

      {

        "name": "Workers AI",

        "link": "https://developers.cloudflare.com/workers-ai/"

      },

      {

        "name": "Vectorize",

        "link": "https://developers.cloudflare.com/vectorize/"

      },

      {

        "name": "AI Gateway",

        "link": "https://developers.cloudflare.com/ai-gateway/"

      },

      {

        "name": "AI Playground",

        "link": "https://playground.ai.cloudflare.com/"

      },

      {

        "name": "Access",

        "link": "https://developers.cloudflare.com/cloudflare-one/access-controls/policies/"

      },

      {

        "name": "Tunnel",

        "link": "https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/"

      },

      {

        "name": "Gateway",

        "link": "https://developers.cloudflare.com/cloudflare-one/traffic-policies/"

      },

      {

        "name": "Browser Isolation",

        "link": "https://developers.cloudflare.com/cloudflare-one/remote-browser-isolation/"

      },

      {

        "name": "Replace your VPN",

        "link": "https://developers.cloudflare.com/learning-paths/replace-vpn/concepts/"

      }

    ]

  }

}


```

Explain Code

Below is an example using the TypeScript SDK:

TypeScript

```

import Cloudflare from "cloudflare";


const client = new Cloudflare({

  apiToken: process.env["CLOUDFLARE_API_TOKEN"], // This is the default and can be omitted

});


const json = await client.browserRendering.json.create({

  account_id: process.env["CLOUDFLARE_ACCOUNT_ID"],

  url: "https://developers.cloudflare.com/",

  prompt: "Get me the list of AI products",

  response_format: {

    type: "json_schema",

    schema: {

      type: "object",

      properties: {

        products: {

          type: "array",

          items: {

            type: "object",

            properties: {

              name: {

                type: "string",

              },

              link: {

                type: "string",

              },

            },

            required: ["name"],

          },

        },

      },

    },

  },

});

console.log(json);


```

Explain Code

## Advanced Usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/json/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Using a custom model (BYO API Key)

Browser Run can use a custom model for which you supply credentials. List the model(s) in the `custom_ai` array:

* `model` should be formed as `<provider>/<model_name>` and the provider must be one of these [supported providers](https://developers.cloudflare.com/ai-gateway/usage/chat-completion/#supported-providers).
* `authorization` is the bearer token or API key that allows Browser Run to call the provider on your behalf.

This example uses the `custom_ai` parameter to instruct Browser Run to use a Anthropic's Claude Sonnet 4 model. The prompt asks the model to extract the main `<h1>` and `<h2>` headings from the target URL and return them in a structured JSON object.

Terminal window

```

curl --request POST \

  --url https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT_ID/browser-rendering/json \

  --header 'authorization: Bearer CF_API_TOKEN' \

  --header 'content-type: application/json' \

  --data '{

  "url": "http://demoto.xyz/headings",

  "prompt": "Get the heading from the page in the form of an object like h1, h2. If there are many headings of the same kind then grab the first one.",

  "response_format": {

    "type": "json_schema",

    "schema": {

      "type": "object",

      "properties": {

        "h1": {

          "type": "string"

        },

        "h2": {

          "type": "string"

        }

      },

      "required": [

        "h1"

      ]

    }

  },

  "custom_ai": [

    {

      "model": "anthropic/claude-sonnet-4-20250514",

      "authorization": "Bearer <ANTHROPIC_API_KEY>"

    }

  ]

}


```

Explain Code

```

{

  "success": true,

  "result": {

    "h1": "Heading 1",

    "h2": "Heading 2"

  }

}


```

### Using a custom model with fallbacks

You may specify multiple models to provide automatic failover. Browser Run will attempt the models in order until one succeeds. To add failover, list additional models in the `custom_ai` array.

In this example, Browser Run first calls Anthropic's Claude Sonnet 4 model. If that request returns an error, it automatically retries with Meta Llama 3.3 70B from [Workers AI](https://developers.cloudflare.com/workers-ai/), then OpenAI's GPT-4o.

```

"custom_ai": [

  {

    "model": "anthropic/claude-sonnet-4-20250514",

    "authorization": "Bearer <ANTHROPIC_API_KEY>"

  },

  {

    "model": "workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast",

    "authorization": "Bearer <CLOUDFLARE_AUTH_TOKEN>"

  },

{

    "model": "openai/gpt-4o",

    "authorization": "Bearer <OPENAI_API_KEY>"

  }

]


```

Explain Code

## Troubleshooting

### JSON extraction returns null or empty results

If the `/json` endpoint returns null or empty results:

* **Provide a clear prompt** — Be specific about what data to extract and where it appears on the page (for example, "Extract the product name, price, and description from the main product section").
* **Define a response schema** — Use `response_format` with a JSON schema to enforce the expected output structure.
* **Use a custom model** — If the default [Workers AI](https://developers.cloudflare.com/workers-ai/) model does not produce the desired results, use the `custom_ai` parameter to specify a different model. Refer to [Using a custom model (BYO API Key)](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/#using-a-custom-model-byo-api-key) for details.

### Handling JavaScript-heavy pages

For JavaScript-heavy pages or Single Page Applications (SPAs), the default page load behavior may return empty or incomplete results. This happens because the browser considers the page loaded before JavaScript has finished rendering the content.

The simplest solution is to use the `gotoOptions.waitUntil` parameter set to `networkidle0` or `networkidle2`:

```

{

  "url": "https://example.com",

  "gotoOptions": {

    "waitUntil": "networkidle0"

  }

}


```

For faster responses, advanced users can use `waitForSelector` to wait for a specific element instead of waiting for all network activity to stop. This requires knowing which CSS selector indicates the content you need has loaded. For more details, refer to [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Set a custom user agent

You can change the user agent at the page level by passing `userAgent` as a top-level parameter in the JSON body. This is useful if the target website serves different content based on the user agent.

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Run requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#non-configurable-headers) rather than relying on the User-Agent string.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/json-endpoint/","name":"/json - Capture structured data using AI"}}]}
```

---

---
title: /links - Retrieve links from a webpage
description: The /links endpoint retrieves all links from a webpage. It can be used to extract all links from a page, including those that are hidden.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/links-endpoint.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /links - Retrieve links from a webpage

The `/links` endpoint retrieves all links from a webpage. It can be used to extract all links from a page, including those that are hidden.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/links


```

## Required fields

You must provide either `url` or `html`:

* `url` (string)
* `html` (string)

## Common use cases

* Collect only user-visible links for UX or SEO analysis
* Crawl a site by discovering links on seed pages
* Validate navigation/footers and detect broken or external links

## Basic usage

### Get all links on a page

* [ curl ](#tab-panel-3584)
* [ TypeScript SDK ](#tab-panel-3585)

This example grabs all links from the [Cloudflare Doc's homepage ↗](https://developers.cloudflare.com/). The response will be a JSON array containing the links found on the page.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/links' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://developers.cloudflare.com/"

  }'


```

```

{

  "success": true,

  "result": [

    "https://developers.cloudflare.com/",

    "https://developers.cloudflare.com/products/",

    "https://developers.cloudflare.com/api/",

    "https://developers.cloudflare.com/fundamentals/api/reference/sdks/",

    "https://dash.cloudflare.com/",

    "https://developers.cloudflare.com/fundamentals/subscriptions-and-billing/",

    "https://developers.cloudflare.com/api/",

    "https://developers.cloudflare.com/changelog/",

64 collapsed lines

    "https://developers.cloudflare.com/glossary/",

    "https://developers.cloudflare.com/reference-architecture/",

    "https://developers.cloudflare.com/web-analytics/",

    "https://developers.cloudflare.com/support/troubleshooting/http-status-codes/",

    "https://developers.cloudflare.com/registrar/",

    "https://developers.cloudflare.com/1.1.1.1/setup/",

    "https://developers.cloudflare.com/workers/",

    "https://developers.cloudflare.com/pages/",

    "https://developers.cloudflare.com/r2/",

    "https://developers.cloudflare.com/images/",

    "https://developers.cloudflare.com/stream/",

    "https://developers.cloudflare.com/products/?product-group=Developer+platform",

    "https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/",

    "https://developers.cloudflare.com/workers-ai/",

    "https://developers.cloudflare.com/vectorize/",

    "https://developers.cloudflare.com/ai-gateway/",

    "https://playground.ai.cloudflare.com/",

    "https://developers.cloudflare.com/products/?product-group=AI",

    "https://developers.cloudflare.com/cloudflare-one/access-controls/policies/",

    "https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/",

    "https://developers.cloudflare.com/cloudflare-one/traffic-policies/",

    "https://developers.cloudflare.com/cloudflare-one/remote-browser-isolation/",

    "https://developers.cloudflare.com/learning-paths/replace-vpn/concepts/",

    "https://developers.cloudflare.com/products/?product-group=Cloudflare+One",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwAmAIyiAzMIAsATlmi5ALhYs2wDnC40+AkeKlyFcgLAAoAMLoqEAKY3sAESgBnGOhdRo1pSXV4CYhIqOGBbBgAiKBpbAA8AOgArFwjSVCgwe1DwqJiE5IjzKxt7CGwAFToYW184GBgwPgIoa2REuAA3OBdeBFgIAGpgdFxwW3NzOPckElxbVDhwCBIAbzMSEm66Kl4-WwheAAsACgRbAEcQWxcIAEpV9Y2SXmsbkkOIYDASBhIAAwAPABCRwAeQs5QAmgAFACi70+YAAfI8NgCKLg6Cink8AYdREiABK2MBgdAkADqmDAuAByHx2JxJABMCR5UOrhIwEQAGsQDASAB3bokADm9lsCAItlw5DomxIFjJIFwqDAiFslMwPMl8TprNRzOQGKxfyIZkNZwgIAQVGCtkFJAAStd3FQXLZjh8vgAaB5M962OBzBAuXxrAMbCIvEoOCBVWwRXwROyxFDesBEI6ID0QBgAVXKADFsAAOCI+w0bAC+lZx1du5prlerRHMqmY6k02h4-CEYkkMnkilkRWsdgczjcHi8LSovn8mlIITCkTChE0qT8GSyq4iZDJZEKlnHpQqCdq9UavGarWS1gmZhWEW50QA+sNRpkk7k5vkUtW7Ydl2gQ9ro-YGEOxiyMwQA",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwB2AMwAWAKyCAjMICc8meIBcLFm2Ac4XGnwEiJ0uYuUBYAFABhdFQgBTO9gAiUAM4x0bqNFsqSmngExCRUcMD2DABEUDT2AB4AdABWblGkqFBgjuGRMXFJqVGWNnaOENgAKnQw9v5wMDBgfARQtsjJcABucG68CLAQANTA6Ljg9paWCZ5IJLj2qHDgECQA3hYkJL10VLwB9hC8ABYAFAj2AI4g9m4QAJTrm1skvLZ388EkDE8vL8f2MBgdD+KIAd0wYFwUQANM8tgBfIgWeEkC4QEAIKgkABKt08VDc9hSblsp2092RiLhSMs6mYmm0uh4-CEYiksgUSnEJVsDicrg8Xh8bSo-kC2lIYQi0QihG06QCWRyMqiZGBZGK1j55SqNTq20azV4rXaqVsUwsayiwDgsQA+qNxtkoip8gtCmkEXT6Yzgsz9GyjJzTOJmEA",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBWABwBGAOyjRANgDMAFgCcygFwsWbYBzhcafASInS5S1QFgAUAGF0VCAFMH2ACJQAzjHQeo0e2ok2ngExCRUcMCODABEUDSOAB4AdABWHjGkqFBgzpHRcQkp6THWdg7OENgAKnQwjoFwMDBgfARQ9sipcABucB68CLAQANTA6LjgjtbWSd5IJLiOqHDgECQA3lYkJP10VLxBjhC8ABYAFAiOAI4gjh4QAJSb2zskyABUH69vHyQASo4WnBeI4SAADK7jJzgkgAdz8pxIEFOYNOPnWdEo8M8SIg6BIHmcuBIV1u9wgHmR6B+Ow+yFpvHsD1JjmhYIYJBipwgEBgHjUyGQSUiLUcySZwEyVlpVwgIAQVF2cLgfiOJwuUPQTgANKzyQ9HkRXgBfHVWE1EayaZjaXT6Hj8IRiKQyBQqZRlexOFzuLw+PwdKiBYK6UgRKKxKKEXSZII5PKRmJkMDoMilWzeyo1OoNXbNVq8dqddL2GZWDYxYCqqgAfXGk1yMTUhSWxQyJutNrtoQdhmdJjd5mUzCAA",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBmACyiAnBMFSAbIICMALhYs2wDnC40+AkeKkyJ8hQFgAUAGF0VCAFNb2ACJQAzjHSuo0G0pLq8AmISKjhgOwYAIigaOwAPADoAK1dI0lQoMAcwiOjYxJTIi2tbBwhsABU6GDs-OBgYMD4CKBtkJLgANzhXXgRYCABqYHRccDsLC3iPJBJcO1Q4cAgSAG9zEhIeuipefzsIXgALAAoEOwBHEDtXCABKNY3Nkl4bW7mb6FCfKgBVACUADIkBgkSJHCAQGCuJTIZDxMKNOwJV7ANJPTavKjvW4EECuazzEEkYSKIgYkjnCAgBBUEj-G4ebHI848c68CAnea3GItGwAwEAGhIuOpBNGdju5M2AF9BeYZUQLKpmOpNNoePwhGJJNI5IpijZ7I4XO5PN5WlQ-AFNKRQuEouFCJo0v5MtkHZEyGB0GQilYjWVKtValsGk1eHyqO1XDZJuZVpFgHAYgB9EZjLKRJR5eYFVIy5UqtVBDW6bUGPXGRTMIA",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwAOAJwBmAIyiATKMkB2AKwyAXCxZtgHOFxp8BIidLmKVAWABQAYXRUIAU3vYAIlADOMdO6jQ7qki08AmISKjhgBwYAIigaBwAPADoAK3do0lQoMCcIqNj45LToq1t7JwhsABU6GAcAuBgYMD4CKDtkFLgANzh3XgRYCABqYHRccAcrK0SvJBJcB1Q4cAgSAG9LEhI+uipeQIcIXgALAAoEBwBHEAd3CABKDa3tnfc9g9RqXj8qEgBZI4ncYAOXQEAAgmAwOgAO4OXAXa63e5PTavV6XCAgBB-KgOWEkABKdy8VHcDjOAANARBgbgSAASdaXG53CBJSJ08YAXzC4J20LhCKSVIANM8MRj7gQQO4AgAWQRKMUvKUkE4OOCLBDyyXq15QmGwgLRADiAFEqtFVQaSDzbVKeQ8iGr7W7kMgSAB5KhgOgkS1VEislEQdwkWGYADWkd8JxIdI8JBgCHQCToSTdUFQJCRbPunKB4xIAEIGAwSOardEnlicX9afSwZChfDEaH2S63fXcYdjucqScIBAYPLPYkIs0HEleOhgFTu9sHZYeUQrBpmFodHoePwhGIpLJ5MoZKU7I5nG5PN5fO0qAEgjpSOFIjEudqQhlAtlcm-omQMJkCUNgXhU1S1PUOxNC0vBtB0aR2NMljrNEwBwHEAD6YwTDk0SqAUixFOkPIbpu24hLuBgHsYx5mDIzBAA",

    "https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/",

    "https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/",

    "https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/",

    "https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/",

    "https://developers.cloudflare.com/waf/custom-rules/use-cases/allow-traffic-from-specific-countries/",

    "https://discord.cloudflare.com/",

    "https://x.com/CloudflareDev",

    "https://community.cloudflare.com/",

    "https://github.com/cloudflare",

    "https://developers.cloudflare.com/sponsorships/",

    "https://developers.cloudflare.com/style-guide/",

    "https://blog.cloudflare.com/",

    "https://developers.cloudflare.com/fundamentals/",

    "https://support.cloudflare.com/",

    "https://www.cloudflarestatus.com/",

    "https://www.cloudflare.com/trust-hub/compliance-resources/",

    "https://www.cloudflare.com/trust-hub/gdpr/",

    "https://www.cloudflare.com/",

    "https://www.cloudflare.com/people/",

    "https://www.cloudflare.com/careers/",

    "https://radar.cloudflare.com/",

    "https://speed.cloudflare.com/",

    "https://isbgpsafeyet.com/",

    "https://rpki.cloudflare.com/",

    "https://ct.cloudflare.com/",

    "https://x.com/cloudflare",

    "http://discord.cloudflare.com/",

    "https://www.youtube.com/cloudflare",

    "https://github.com/cloudflare/cloudflare-docs",

    "https://www.cloudflare.com/privacypolicy/",

    "https://www.cloudflare.com/website-terms/",

    "https://www.cloudflare.com/disclosure/",

    "https://www.cloudflare.com/trademark/"

  ]

}


```

Explain Code

TypeScript

```

import Cloudflare from "cloudflare";


const client = new Cloudflare({

  apiToken: process.env["CLOUDFLARE_API_TOKEN"],

});


const links = await client.browserRendering.links.create({

  account_id: process.env["CLOUDFLARE_ACCOUNT_ID"],

  url: "https://developers.cloudflare.com/",

});


console.log(links);


```

Explain Code

## Advanced usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/links/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Retrieve only visible links

Set the `visibleLinksOnly` parameter to `true` to only return links that are visible on the page. By default, this is set to `false`.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/links' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://developers.cloudflare.com/",

    "visibleLinksOnly": true

  }'


```

```

{

  "success": true,

  "result": [

    "https://developers.cloudflare.com/",

    "https://developers.cloudflare.com/products/",

    "https://developers.cloudflare.com/api/",

    "https://developers.cloudflare.com/fundamentals/api/reference/sdks/",

    "https://dash.cloudflare.com/",

    "https://developers.cloudflare.com/fundamentals/subscriptions-and-billing/",

    "https://developers.cloudflare.com/api/",

    "https://developers.cloudflare.com/changelog/",

64 collapsed lines

    "https://developers.cloudflare.com/glossary/",

    "https://developers.cloudflare.com/reference-architecture/",

    "https://developers.cloudflare.com/web-analytics/",

    "https://developers.cloudflare.com/support/troubleshooting/http-status-codes/",

    "https://developers.cloudflare.com/registrar/",

    "https://developers.cloudflare.com/1.1.1.1/setup/",

    "https://developers.cloudflare.com/workers/",

    "https://developers.cloudflare.com/pages/",

    "https://developers.cloudflare.com/r2/",

    "https://developers.cloudflare.com/images/",

    "https://developers.cloudflare.com/stream/",

    "https://developers.cloudflare.com/products/?product-group=Developer+platform",

    "https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/",

    "https://developers.cloudflare.com/workers-ai/",

    "https://developers.cloudflare.com/vectorize/",

    "https://developers.cloudflare.com/ai-gateway/",

    "https://playground.ai.cloudflare.com/",

    "https://developers.cloudflare.com/products/?product-group=AI",

    "https://developers.cloudflare.com/cloudflare-one/access-controls/policies/",

    "https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/",

    "https://developers.cloudflare.com/cloudflare-one/traffic-policies/",

    "https://developers.cloudflare.com/cloudflare-one/remote-browser-isolation/",

    "https://developers.cloudflare.com/learning-paths/replace-vpn/concepts/",

    "https://developers.cloudflare.com/products/?product-group=Cloudflare+One",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwAmAIyiAzMIAsATlmi5ALhYs2wDnC40+AkeKlyFcgLAAoAMLoqEAKY3sAESgBnGOhdRo1pSXV4CYhIqOGBbBgAiKBpbAA8AOgArFwjSVCgwe1DwqJiE5IjzKxt7CGwAFToYW184GBgwPgIoa2REuAA3OBdeBFgIAGpgdFxwW3NzOPckElxbVDhwCBIAbzMSEm66Kl4-WwheAAsACgRbAEcQWxcIAEpV9Y2SXmsbkkOIYDASBhIAAwAPABCRwAeQs5QAmgAFACi70+YAAfI8NgCKLg6Cink8AYdREiABK2MBgdAkADqmDAuAByHx2JxJABMCR5UOrhIwEQAGsQDASAB3bokADm9lsCAItlw5DomxIFjJIFwqDAiFslMwPMl8TprNRzOQGKxfyIZkNZwgIAQVGCtkFJAAStd3FQXLZjh8vgAaB5M962OBzBAuXxrAMbCIvEoOCBVWwRXwROyxFDesBEI6ID0QBgAVXKADFsAAOCI+w0bAC+lZx1du5prlerRHMqmY6k02h4-CEYkkMnkilkRWsdgczjcHi8LSovn8mlIITCkTChE0qT8GSyq4iZDJZEKlnHpQqCdq9UavGarWS1gmZhWEW50QA+sNRpkk7k5vkUtW7Ydl2gQ9ro-YGEOxiyMwQA",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwB2AMwAWAKyCAjMICc8meIBcLFm2Ac4XGnwEiJ0uYuUBYAFABhdFQgBTO9gAiUAM4x0bqNFsqSmngExCRUcMD2DABEUDT2AB4AdABWblGkqFBgjuGRMXFJqVGWNnaOENgAKnQw9v5wMDBgfARQtsjJcABucG68CLAQANTA6Ljg9paWCZ5IJLj2qHDgECQA3hYkJL10VLwB9hC8ABYAFAj2AI4g9m4QAJTrm1skvLZ388EkDE8vL8f2MBgdD+KIAd0wYFwUQANM8tgBfIgWeEkC4QEAIKgkABKt08VDc9hSblsp2092RiLhSMs6mYmm0uh4-CEYiksgUSnEJVsDicrg8Xh8bSo-kC2lIYQi0QihG06QCWRyMqiZGBZGK1j55SqNTq20azV4rXaqVsUwsayiwDgsQA+qNxtkoip8gtCmkEXT6Yzgsz9GyjJzTOJmEA",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBWABwBGAOyjRANgDMAFgCcygFwsWbYBzhcafASInS5S1QFgAUAGF0VCAFMH2ACJQAzjHQeo0e2ok2ngExCRUcMCODABEUDSOAB4AdABWHjGkqFBgzpHRcQkp6THWdg7OENgAKnQwjoFwMDBgfARQ9sipcABucB68CLAQANTA6LjgjtbWSd5IJLiOqHDgECQA3lYkJP10VLxBjhC8ABYAFAiOAI4gjh4QAJSb2zskyABUH69vHyQASo4WnBeI4SAADK7jJzgkgAdz8pxIEFOYNOPnWdEo8M8SIg6BIHmcuBIV1u9wgHmR6B+Ow+yFpvHsD1JjmhYIYJBipwgEBgHjUyGQSUiLUcySZwEyVlpVwgIAQVF2cLgfiOJwuUPQTgANKzyQ9HkRXgBfHVWE1EayaZjaXT6Hj8IRiKQyBQqZRlexOFzuLw+PwdKiBYK6UgRKKxKKEXSZII5PKRmJkMDoMilWzeyo1OoNXbNVq8dqddL2GZWDYxYCqqgAfXGk1yMTUhSWxQyJutNrtoQdhmdJjd5mUzCAA",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBmACyiAnBMFSAbIICMALhYs2wDnC40+AkeKkyJ8hQFgAUAGF0VCAFNb2ACJQAzjHSuo0G0pLq8AmISKjhgOwYAIigaOwAPADoAK1dI0lQoMAcwiOjYxJTIi2tbBwhsABU6GDs-OBgYMD4CKBtkJLgANzhXXgRYCABqYHRccDsLC3iPJBJcO1Q4cAgSAG9zEhIeuipefzsIXgALAAoEOwBHEDtXCABKNY3Nkl4bW7mb6FCfKgBVACUADIkBgkSJHCAQGCuJTIZDxMKNOwJV7ANJPTavKjvW4EECuazzEEkYSKIgYkjnCAgBBUEj-G4ebHI848c68CAnea3GItGwAwEAGhIuOpBNGdju5M2AF9BeYZUQLKpmOpNNoePwhGJJNI5IpijZ7I4XO5PN5WlQ-AFNKRQuEouFCJo0v5MtkHZEyGB0GQilYjWVKtValsGk1eHyqO1XDZJuZVpFgHAYgB9EZjLKRJR5eYFVIy5UqtVBDW6bUGPXGRTMIA",

    "https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwAOAJwBmAIyiATKMkB2AKwyAXCxZtgHOFxp8BIidLmKVAWABQAYXRUIAU3vYAIlADOMdO6jQ7qki08AmISKjhgBwYAIigaBwAPADoAK3do0lQoMCcIqNj45LToq1t7JwhsABU6GAcAuBgYMD4CKDtkFLgANzh3XgRYCABqYHRccAcrK0SvJBJcB1Q4cAgSAG9LEhI+uipeQIcIXgALAAoEBwBHEAd3CABKDa3tnfc9g9RqXj8qEgBZI4ncYAOXQEAAgmAwOgAO4OXAXa63e5PTavV6XCAgBB-KgOWEkABKdy8VHcDjOAANARBgbgSAASdaXG53CBJSJ08YAXzC4J20LhCKSVIANM8MRj7gQQO4AgAWQRKMUvKUkE4OOCLBDyyXq15QmGwgLRADiAFEqtFVQaSDzbVKeQ8iGr7W7kMgSAB5KhgOgkS1VEislEQdwkWGYADWkd8JxIdI8JBgCHQCToSTdUFQJCRbPunKB4xIAEIGAwSOardEnlicX9afSwZChfDEaH2S63fXcYdjucqScIBAYPLPYkIs0HEleOhgFTu9sHZYeUQrBpmFodHoePwhGIpLJ5MoZKU7I5nG5PN5fO0qAEgjpSOFIjEudqQhlAtlcm-omQMJkCUNgXhU1S1PUOxNC0vBtB0aR2NMljrNEwBwHEAD6YwTDk0SqAUixFOkPIbpu24hLuBgHsYx5mDIzBAA",

    "https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/",

    "https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/",

    "https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/",

    "https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/",

    "https://developers.cloudflare.com/waf/custom-rules/use-cases/allow-traffic-from-specific-countries/",

    "https://discord.cloudflare.com/",

    "https://x.com/CloudflareDev",

    "https://community.cloudflare.com/",

    "https://github.com/cloudflare",

    "https://developers.cloudflare.com/sponsorships/",

    "https://developers.cloudflare.com/style-guide/",

    "https://blog.cloudflare.com/",

    "https://developers.cloudflare.com/fundamentals/",

    "https://support.cloudflare.com/",

    "https://www.cloudflarestatus.com/",

    "https://www.cloudflare.com/trust-hub/compliance-resources/",

    "https://www.cloudflare.com/trust-hub/gdpr/",

    "https://www.cloudflare.com/",

    "https://www.cloudflare.com/people/",

    "https://www.cloudflare.com/careers/",

    "https://radar.cloudflare.com/",

    "https://speed.cloudflare.com/",

    "https://isbgpsafeyet.com/",

    "https://rpki.cloudflare.com/",

    "https://ct.cloudflare.com/",

    "https://x.com/cloudflare",

    "http://discord.cloudflare.com/",

    "https://www.youtube.com/cloudflare",

    "https://github.com/cloudflare/cloudflare-docs",

    "https://www.cloudflare.com/privacypolicy/",

    "https://www.cloudflare.com/website-terms/",

    "https://www.cloudflare.com/disclosure/",

    "https://www.cloudflare.com/trademark/"

  ]

}


```

Explain Code

### Retrieve only links from the same domain

Set the `excludeExternalLinks` parameter to `true` to exclude links pointing to external domains. By default, this is set to `false`.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/links' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://developers.cloudflare.com/",

    "excludeExternalLinks": true

  }'


```

### Handling JavaScript-heavy pages

For JavaScript-heavy pages or Single Page Applications (SPAs), the default page load behavior may return empty or incomplete results. This happens because the browser considers the page loaded before JavaScript has finished rendering the content.

The simplest solution is to use the `gotoOptions.waitUntil` parameter set to `networkidle0` or `networkidle2`:

```

{

  "url": "https://example.com",

  "gotoOptions": {

    "waitUntil": "networkidle0"

  }

}


```

For faster responses, advanced users can use `waitForSelector` to wait for a specific element instead of waiting for all network activity to stop. This requires knowing which CSS selector indicates the content you need has loaded. For more details, refer to [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Set a custom user agent

You can change the user agent at the page level by passing `userAgent` as a top-level parameter in the JSON body. This is useful if the target website serves different content based on the user agent.

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Run requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#non-configurable-headers) rather than relying on the User-Agent string.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/links-endpoint/","name":"/links - Retrieve links from a webpage"}}]}
```

---

---
title: /markdown - Extract Markdown from a webpage
description: The /markdown endpoint retrieves a webpage's content and converts it into Markdown format. You can specify a URL and optional parameters to refine the extraction process.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/markdown-endpoint.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /markdown - Extract Markdown from a webpage

The `/markdown` endpoint retrieves a webpage's content and converts it into Markdown format. You can specify a URL and optional parameters to refine the extraction process.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/markdown


```

## Required fields

You must provide either `url` or `html`:

* `url` (string)
* `html` (string)

## Common use cases

* Normalize content for downstream processing (summaries, diffs, embeddings)
* Save articles or docs for editing or storage
* Strip styling/scripts and keep readable content + links

## Basic usage

### Convert a URL to Markdown

* [ curl ](#tab-panel-3586)
* [ TypeScript SDK ](#tab-panel-3587)

This example fetches the Markdown representation of a webpage.

Terminal window

```

curl -X 'POST' 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/markdown' \

  -H 'Content-Type: application/json' \

  -H 'Authorization: Bearer <apiToken>' \

  -d '{

    "url": "https://example.com"

  }'


```

```

{

  "success": true,

  "result": "# Example Domain\n\nThis domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.\n\n[More information...](https://www.iana.org/domains/example)"

}


```

TypeScript

```

import Cloudflare from "cloudflare";


const client = new Cloudflare({

  apiToken: process.env["CLOUDFLARE_API_TOKEN"],

});


const markdown = await client.browserRendering.markdown.create({

  account_id: process.env["CLOUDFLARE_ACCOUNT_ID"],

  url: "https://developers.cloudflare.com/",

});


console.log(markdown);


```

Explain Code

### Convert raw HTML to Markdown

Instead of fetching the content by specifying the URL, you can provide raw HTML content directly.

Terminal window

```

curl -X 'POST' 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/markdown' \

  -H 'Content-Type: application/json' \

  -H 'Authorization: Bearer <apiToken>' \

  -d '{

    "html": "<div>Hello World</div>"

  }'


```

```

{

  "success": true,

  "result": "Hello World"

}


```

## Advanced usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/markdown/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Exclude unwanted requests (for example, CSS)

You can refine the Markdown extraction by using the `rejectRequestPattern` parameter. In this example, requests matching the given regex pattern (such as CSS files) are excluded.

Terminal window

```

curl -X 'POST' 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/markdown' \

  -H 'Content-Type: application/json' \

  -H 'Authorization: Bearer <apiToken>' \

  -d '{

    "url": "https://example.com",

    "rejectRequestPattern": ["/^.*\\.(css)/"]

  }'


```

```

{

  "success": true,

  "result": "# Example Domain\n\nThis domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.\n\n[More information...](https://www.iana.org/domains/example)"

}


```

### Handling JavaScript-heavy pages

For JavaScript-heavy pages or Single Page Applications (SPAs), the default page load behavior may return empty or incomplete results. This happens because the browser considers the page loaded before JavaScript has finished rendering the content.

The simplest solution is to use the `gotoOptions.waitUntil` parameter set to `networkidle0` or `networkidle2`:

```

{

  "url": "https://example.com",

  "gotoOptions": {

    "waitUntil": "networkidle0"

  }

}


```

For faster responses, advanced users can use `waitForSelector` to wait for a specific element instead of waiting for all network activity to stop. This requires knowing which CSS selector indicates the content you need has loaded. For more details, refer to [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Set a custom user agent

You can change the user agent at the page level by passing `userAgent` as a top-level parameter in the JSON body. This is useful if the target website serves different content based on the user agent.

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Run requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#non-configurable-headers) rather than relying on the User-Agent string.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

## Other Markdown conversion features

* Workers AI [AI.toMarkdown()](https://developers.cloudflare.com/workers-ai/features/markdown-conversion/) supports multiple document types and summarization.
* [Markdown for Agents](https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/) allows real-time document conversion for Cloudflare zones using content negotiation headers.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/markdown-endpoint/","name":"/markdown - Extract Markdown from a webpage"}}]}
```

---

---
title: /pdf - Render PDF
description: The /pdf endpoint instructs the browser to generate a PDF of a webpage or custom HTML using Cloudflare's headless Browser Run service.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/pdf-endpoint.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /pdf - Render PDF

The `/pdf` endpoint instructs the browser to generate a PDF of a webpage or custom HTML using Cloudflare's headless Browser Run service.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/pdf


```

## Required fields

You must provide either `url` or `html`:

* `url` (string)
* `html` (string)

## Common use cases

* Capture a PDF of a webpage
* Generate PDFs, such as invoices, licenses, reports, and certificates, directly from HTML

## Basic usage

### Convert a URL to PDF

* [ curl ](#tab-panel-3588)
* [ TypeScript SDK ](#tab-panel-3589)

Navigate to `https://example.com/` and inject custom CSS and an external stylesheet. Then return the rendered page as a PDF.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/pdf' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/",

    "addStyleTag": [

      { "content": "body { font-family: Arial; }" }

    ]

  }' \

  --output "output.pdf"


```

Explain Code

TypeScript

```

import Cloudflare from "cloudflare";


const client = new Cloudflare({

  apiToken: process.env["CLOUDFLARE_API_TOKEN"],

});


const pdf = await client.browserRendering.pdf.create({

  account_id: process.env["CLOUDFLARE_ACCOUNT_ID"],

  url: "https://example.com/",

  addStyleTag: [{ content: "body { font-family: Arial; }" }],

});


console.log(pdf);


const content = await pdf.blob();

console.log(content);


```

Explain Code

### Convert custom HTML to PDF

If you have raw HTML you want to generate a PDF from, use the `html` option. You can still apply custom styles using the `addStyleTag` parameter.

Terminal window

```

curl -X POST https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/pdf \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

  "html": "<html><body>Advanced Snapshot</body></html>",

  "addStyleTag": [

      { "content": "body { font-family: Arial; }" },

      { "url": "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" }

    ]

}' \

  --output "invoice.pdf"


```

Explain Code

Request size limits

The PDF endpoint accepts request bodies up to 50 MB. Requests larger than this will fail with `Error: request entity too large`.

## Advanced usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/pdf/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Advanced page load with custom headers and viewport

Navigate to `https://example.com`, setting additional HTTP headers and configuring the page size (viewport). The PDF generation will wait until there are no more than two network connections for at least 500 ms, or until the maximum timeout of 4500 ms is reached, before rendering.

The `goToOptions` parameter exposes most of [Puppeteer's API ↗](https://pptr.dev/api/puppeteer.gotooptions).

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/pdf' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/",

    "setExtraHTTPHeaders": {

      "X-Custom-Header": "value"

    },

    "viewport": {

      "width": 1200,

      "height": 800

    },

    "gotoOptions": {

      "waitUntil": "networkidle2",

      "timeout": 45000

    }

  }' \

  --output "advanced-output.pdf"


```

Explain Code

### Blocking images and styles when generating a PDF

The options `rejectResourceTypes` and `rejectRequestPattern` can be used to block requests during rendering. The opposite can also be done, _only_ allow certain requests using `allowResourceTypes` and `allowRequestPattern`.

Terminal window

```

curl -X POST https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/pdf \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

  "url": "https://cloudflare.com/",

  "rejectResourceTypes": ["image"],

  "rejectRequestPattern": ["/^.*\\.(css)"]

}' \

  --output "cloudflare.pdf"


```

### Customize page headers and footers

You can customize page headers and footers with HTML templates using the `headerTemplate` and `footerTemplate` options. Enable `displayHeaderFooter` to include them in your output. This example generates an A5 PDF with a branded header, a footer message, and page numbering.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/pdf' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com",

    "pdfOptions": {

      "format": "a5",

      "headerTemplate": "<div style=\"font-size: 10px; text-align: center; width: 100%; padding: 5px;\"><span>brand name</span></div>",

      "displayHeaderFooter": true,

      "footerTemplate": "<div style=\"color: lightgray; border-top: solid lightgray 1px; font-size: 10px; padding-top: 5px; text-align: center; width: 100%;\"><span>This is a test message</span> - <span class=\"pageNumber\"></span></div>",

      "margin": {

        "top": "70px",

        "bottom": "70px"

      }

    }

  }' \

  --output "header-footer.pdf"


```

Explain Code

### Include dynamic placeholders from page metadata

You can include dynamic placeholders such as `title`, `date`, `pageNumber`, and `totalPages` in the header or footer to display metadata on each page. This example produces an A4 PDF with a company-branded header, current date and title, and page numbering in the footer.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/pdf' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://news.ycombinator.com",

    "pdfOptions": {

      "format": "a4",

      "landscape": false,

      "printBackground": true,

      "preferCSSPageSize": true,

      "displayHeaderFooter": true,

      "scale": 1.0,

      "headerTemplate": "<div style=\"width: 100%; font-size: 10px; padding: 10px; text-align: center;\"><div style=\"border-bottom: 1px solid #ddd;\"><span style=\"color: #666;\">Company Name</span> | <span class=\"date\"></span> | <span class=\"title\"></span></div></div>",

      "footerTemplate": "<div style=\"width: 100%; font-size: 10px; padding: 10px; text-align: center;\"><div style=\"border-top: 1px solid #ddd;\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div></div>",

      "margin": {

        "top": "100px",

        "bottom": "80px",

        "right": "30px",

        "left": "30px"

      },

      "timeout": 30000

    }

  }' \

  --output "dynamic-header-footer.pdf"


```

Explain Code

### Use custom fonts

If your PDF requires a font that is not pre-installed in the Browser Run environment, you can load custom fonts using the `addStyleTag` parameter. For instructions and examples, refer to [Use your own custom font](https://developers.cloudflare.com/browser-run/features/custom-fonts/#quick-actions).

### Handling JavaScript-heavy pages

For JavaScript-heavy pages or Single Page Applications (SPAs), the default page load behavior may return empty or incomplete results. This happens because the browser considers the page loaded before JavaScript has finished rendering the content.

The simplest solution is to use the `gotoOptions.waitUntil` parameter set to `networkidle0` or `networkidle2`:

```

{

  "url": "https://example.com",

  "gotoOptions": {

    "waitUntil": "networkidle0"

  }

}


```

For faster responses, advanced users can use `waitForSelector` to wait for a specific element instead of waiting for all network activity to stop. This requires knowing which CSS selector indicates the content you need has loaded. For more details, refer to [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Set a custom user agent

You can change the user agent at the page level by passing `userAgent` as a top-level parameter in the JSON body. This is useful if the target website serves different content based on the user agent.

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Run requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#non-configurable-headers) rather than relying on the User-Agent string.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/pdf-endpoint/","name":"/pdf - Render PDF"}}]}
```

---

---
title: /scrape - Scrape HTML elements
description: The /scrape endpoint extracts structured data from specific elements on a webpage, returning details such as element dimensions and inner HTML.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/scrape-endpoint.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /scrape - Scrape HTML elements

The `/scrape` endpoint extracts structured data from specific elements on a webpage, returning details such as element dimensions and inner HTML.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/scrape


```

## Required fields

You must provide either `url` or `elements`:

* `url` (string)
* `elements` (array of objects) — each object must include `selector` (string)

## Common use cases

* Extract headings, links, prices, or other repeated content with CSS selectors
* Collect metadata (for example, titles, descriptions, canonical links)

## Basic usage

### Extract headings and links from a URL

* [ curl ](#tab-panel-3590)
* [ TypeScript SDK ](#tab-panel-3591)

Go to `https://example.com` and extract metadata from all `h1` and `a` elements in the DOM.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/scrape' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

  "url": "https://example.com/",

  "elements": [{

    "selector": "h1"

  },

  {

    "selector": "a"

  }]

}'


```

Explain Code

```

{

  "success": true,

  "result": [

    {

      "results": [

        {

          "attributes": [],

          "height": 39,

          "html": "Example Domain",

          "left": 100,

          "text": "Example Domain",

          "top": 133.4375,

          "width": 600

        }

      ],

      "selector": "h1"

    },

    {

      "results": [

        {

          "attributes": [

            { "name": "href", "value": "https://www.iana.org/domains/example" }

          ],

          "height": 20,

          "html": "More information...",

          "left": 100,

          "text": "More information...",

          "top": 249.875,

          "width": 142

        }

      ],

      "selector": "a"

    }

  ]

}


```

Explain Code

TypeScript

```

import Cloudflare from "cloudflare";


const client = new Cloudflare({

  apiToken: process.env["CLOUDFLARE_API_TOKEN"],

});


const scrapes = await client.browserRendering.scrape.create({

  account_id: process.env["CLOUDFLARE_ACCOUNT_ID"],

  elements: [{ selector: "h1" }, { selector: "a" }],

});


console.log(scrapes);


```

Explain Code

Many more options exist, like setting HTTP credentials using `authenticate`, setting `cookies`, and using `gotoOptions` to control page load behaviour - check the endpoint [reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/scrape/methods/create/) for all available parameters.

### Response fields

* `results` _(array of objects)_ \- Contains extracted data for each selector.  
   * `selector` _(string)_ \- The CSS selector used.  
   * `results` _(array of objects)_ \- List of extracted elements matching the selector.  
         * `text` _(string)_ \- Inner text of the element.  
         * `html` _(string)_ \- Inner HTML of the element.  
         * `attributes` _(array of objects)_ \- List of extracted attributes such as `href` for links.  
         * `height`, `width`, `top`, `left` _(number)_ \- Position and dimensions of the element.

## Advanced Usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/scrape/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Handling JavaScript-heavy pages

For JavaScript-heavy pages or Single Page Applications (SPAs), the default page load behavior may return empty or incomplete results. This happens because the browser considers the page loaded before JavaScript has finished rendering the content.

The simplest solution is to use the `gotoOptions.waitUntil` parameter set to `networkidle0` or `networkidle2`:

```

{

  "url": "https://example.com",

  "gotoOptions": {

    "waitUntil": "networkidle0"

  }

}


```

For faster responses, advanced users can use `waitForSelector` to wait for a specific element instead of waiting for all network activity to stop. This requires knowing which CSS selector indicates the content you need has loaded. For more details, refer to [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Set a custom user agent

You can change the user agent at the page level by passing `userAgent` as a top-level parameter in the JSON body. This is useful if the target website serves different content based on the user agent.

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Run requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#non-configurable-headers) rather than relying on the User-Agent string.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/scrape-endpoint/","name":"/scrape - Scrape HTML elements"}}]}
```

---

---
title: /screenshot - Capture screenshot
description: The /screenshot endpoint renders the webpage by processing its HTML and JavaScript, then captures a screenshot of the fully rendered page.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/screenshot-endpoint.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /screenshot - Capture screenshot

The `/screenshot` endpoint renders the webpage by processing its HTML and JavaScript, then captures a screenshot of the fully rendered page.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot


```

## Required fields

You must provide either `url` or `html`:

* `url` (string)
* `html` (string)

## Common use cases

* Generate previews for websites, dashboards, or reports
* Capture screenshots for automated testing, QA, or visual regression

## Basic usage

### Take a screenshot from custom HTML

* [ curl ](#tab-panel-3592)
* [ TypeScript SDK ](#tab-panel-3593)

Sets the HTML content of the page to `Hello World!` and then takes a screenshot. The option `omitBackground` hides the default white background and allows capturing screenshots with transparency.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "html": "Hello World!",

    "screenshotOptions": {

      "omitBackground": true

    }

  }' \

  --output "screenshot.png"


```

Explain Code

TypeScript

```

import Cloudflare from "cloudflare";


const client = new Cloudflare({

  apiToken: process.env["CLOUDFLARE_API_TOKEN"],

});


const screenshot = await client.browserRendering.screenshot.create({

  account_id: process.env["CLOUDFLARE_ACCOUNT_ID"],

  html: "Hello World!",

  screenshotOptions: {

    omitBackground: true,

  },

});


console.log(screenshot.status);


```

Explain Code

### Take a screenshot from a URL

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com"

  }' \

  --output "screenshot.png"


```

For more options to control the final screenshot, like `clip`, `captureBeyondViewport`, `fullPage` and others, check the endpoint [reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/screenshot/methods/create/).

Notes for basic usage

* The `quality` parameter is not compatible with the default `.png` format and will return a 400 error. If you set `quality`, you must also set `type` to `.jpeg` or another supported format.
* By default, the browser viewport is set to **1920×1080**. You can override the default via request options.

## Advanced usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/screenshot/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Capture a screenshot of an authenticated page

Some webpages require authentication before you can view their content. Browser Run supports three authentication methods, which work across all [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) endpoints. For a quick reference of all methods, refer to [How do I render authenticated pages using Quick Actions?](https://developers.cloudflare.com/browser-run/faq/#how-do-i-render-authenticated-pages-using-quick-actions).

#### Cookie-based authentication

Provide valid session cookies to access pages that require login:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/protected-page",

    "cookies": [

      {

        "name": "session_id",

        "value": "your-session-cookie-value",

        "domain": "example.com",

        "path": "/"

      }

    ]

  }' \

  --output "authenticated-screenshot.png"


```

Explain Code

#### HTTP Basic Auth

Use the `authenticate` parameter for pages behind HTTP Basic Authentication:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/protected-page",

    "authenticate": {

      "username": "user",

      "password": "pass"

    }

  }' \

  --output "authenticated-screenshot.png"


```

Explain Code

#### Token-based authentication

Add custom authorization headers using `setExtraHTTPHeaders`:

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/protected-page",

    "setExtraHTTPHeaders": {

      "Authorization": "Bearer your-token"

    }

  }' \

  --output "authenticated-screenshot.png"


```

Explain Code

### Navigate and capture a full-page screenshot

Navigate to `https://cloudflare.com/`, change the page size (`viewport`) and wait until there are no active network connections (`waitUntil`) or up to a maximum of `4500ms` (`timeout`) before capturing a `fullPage` screenshot.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://cloudflare.com/",

    "screenshotOptions": {

       "fullPage": true

    },

    "viewport": {

      "width": 1280,

      "height": 720

    },

    "gotoOptions": {

      "waitUntil": "networkidle0",

      "timeout": 45000

    }

  }' \

  --output "advanced-screenshot.png"


```

Explain Code

### Improve blurry screenshot resolution

If you set a large viewport width and height, your screenshot may appear blurry or pixelated. This can happen if your browser's default `deviceScaleFactor` (which defaults to 1) is not high enough for the viewport.

To fix this, increase the value of the `deviceScaleFactor`.

```

{

  "url": "https://cloudflare.com/",

  "viewport": {

    "width": 3600,

    "height": 2400,

    "deviceScaleFactor": 2

  }

}


```

### Customize CSS and embed custom JavaScript

Instruct the browser to go to `https://example.com`, embed custom JavaScript (`addScriptTag`) and add extra styles (`addStyleTag`), both inline (`addStyleTag.content`) and by loading an external stylesheet (`addStyleTag.url`).

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/",

    "addScriptTag": [

      { "content": "document.querySelector(`h1`).innerText = `Hello World!!!`" }

    ],

    "addStyleTag": [

      {

        "content": "div { background: linear-gradient(45deg, #2980b9  , #82e0aa  ); }"

      },

      {

        "url": "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"

      }

    ]

  }' \

  --output "screenshot.png"


```

Explain Code

### Capture a specific element using the selector option

To capture a screenshot of a specific element on a webpage, use the `selector` option with a valid CSS selector. You can also configure the `viewport` to control the page dimensions during rendering.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com",

    "selector": "#example_element_name",

    "viewport": {

      "width": 1200,

      "height": 1600

    }

  }' \

  --output "screenshot.png"


```

Explain Code

Many more options exist, like setting HTTP credentials using `authenticate`, setting `cookies`, and using `gotoOptions` to control page load behaviour - check the endpoint [reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/screenshot/methods/create/) for all available parameters.

### Handling JavaScript-heavy pages

For JavaScript-heavy pages or Single Page Applications (SPAs), the default page load behavior may return empty or incomplete results. This happens because the browser considers the page loaded before JavaScript has finished rendering the content.

The simplest solution is to use the `gotoOptions.waitUntil` parameter set to `networkidle0` or `networkidle2`:

```

{

  "url": "https://example.com",

  "gotoOptions": {

    "waitUntil": "networkidle0"

  }

}


```

For faster responses, advanced users can use `waitForSelector` to wait for a specific element instead of waiting for all network activity to stop. This requires knowing which CSS selector indicates the content you need has loaded. For more details, refer to [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Set a custom user agent

You can change the user agent at the page level by passing `userAgent` as a top-level parameter in the JSON body. This is useful if the target website serves different content based on the user agent.

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Run requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#non-configurable-headers) rather than relying on the User-Agent string.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/screenshot-endpoint/","name":"/screenshot - Capture screenshot"}}]}
```

---

---
title: /snapshot - Take a webpage snapshot
description: The /snapshot endpoint captures both the HTML content and a screenshot of the webpage in one request. It returns the HTML as a text string and the screenshot as a Base64-encoded image.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/quick-actions/snapshot.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# /snapshot - Take a webpage snapshot

The `/snapshot` endpoint captures both the HTML content and a screenshot of the webpage in one request. It returns the HTML as a text string and the screenshot as a Base64-encoded image.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Endpoint

```

https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/snapshot


```

## Required fields

You must provide either `url` or `html`:

* `url` (string)
* `html` (string)

## Common use cases

* Capture both the rendered HTML and a visual screenshot in a single API call
* Archive pages with visual and structural data together
* Build monitoring tools that compare visual and DOM differences over time

## Basic usage

### Capture a snapshot from a URL

* [ curl ](#tab-panel-3594)
* [ TypeScript SDK ](#tab-panel-3595)

1. Go to `https://example.com/`.
2. Inject custom JavaScript.
3. Capture the rendered HTML.
4. Take a screenshot.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/snapshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/",

    "addScriptTag": [

      { "content": "document.body.innerHTML = \"Snapshot Page\";" }

    ]

  }'


```

```

{

  "success": true,

  "result": {

    "screenshot": "Base64EncodedScreenshotString",

    "content": "<html>...</html>"

  }

}


```

TypeScript

```

import Cloudflare from "cloudflare";


const client = new Cloudflare({

  apiToken: process.env["CLOUDFLARE_API_TOKEN"],

});


const snapshot = await client.browserRendering.snapshot.create({

  account_id: process.env["CLOUDFLARE_ACCOUNT_ID"],

  url: "https://example.com/",

  addScriptTag: [{ content: 'document.body.innerHTML = "Snapshot Page";' }],

});


console.log(snapshot.content);


```

Explain Code

## Advanced usage

Looking for more parameters?

Visit the [Browser Run API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/subresources/snapshot/methods/create/) for all available parameters, such as setting HTTP credentials using `authenticate`, setting `cookies`, and customizing load behavior using `gotoOptions`.

### Create a snapshot from custom HTML

The `html` property in the JSON payload, it sets the html to `<html><body>Advanced Snapshot</body></html>` then does the following steps:

1. Disable JavaScript.
2. Sets the screenshot to `fullPage`.
3. Changes the page size `(viewport)`.
4. Waits up to `30000ms` or until the `DOMContentLoaded` event fires.
5. Returns the rendered HTML content and a base-64 encoded screenshot of the page.

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/snapshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "html": "<html><body>Advanced Snapshot</body></html>",

    "setJavaScriptEnabled": false,

    "screenshotOptions": {

       "fullPage": true

    },

    "viewport": {

      "width": 1200,

      "height": 800

    },

    "gotoOptions": {

      "waitUntil": "domcontentloaded",

      "timeout": 30000

    }

  }'


```

Explain Code

```

{

  "success": true,

  "result": {

    "screenshot": "AdvancedBase64Screenshot",

    "content": "<html><body>Advanced Snapshot</body></html>"

  }

}


```

### Improve blurry screenshot resolution

If you set a large viewport width and height, your screenshot may appear blurry or pixelated. This can happen if your browser's default `deviceScaleFactor` (which defaults to 1) is not high enough for the viewport.

To fix this, increase the value of the `deviceScaleFactor`.

```

{

  "url": "https://cloudflare.com/",

  "viewport": {

    "width": 3600,

    "height": 2400,

    "deviceScaleFactor": 2

  }

}


```

### Handling JavaScript-heavy pages

For JavaScript-heavy pages or Single Page Applications (SPAs), the default page load behavior may return empty or incomplete results. This happens because the browser considers the page loaded before JavaScript has finished rendering the content.

The simplest solution is to use the `gotoOptions.waitUntil` parameter set to `networkidle0` or `networkidle2`:

```

{

  "url": "https://example.com",

  "gotoOptions": {

    "waitUntil": "networkidle0"

  }

}


```

For faster responses, advanced users can use `waitForSelector` to wait for a specific element instead of waiting for all network activity to stop. This requires knowing which CSS selector indicates the content you need has loaded. For more details, refer to [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Set a custom user agent

You can change the user agent at the page level by passing `userAgent` as a top-level parameter in the JSON body. This is useful if the target website serves different content based on the user agent.

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Run requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#non-configurable-headers) rather than relying on the User-Agent string.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/quick-actions/","name":"Quick Actions"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/quick-actions/snapshot/","name":"/snapshot - Take a webpage snapshot"}}]}
```

---

---
title: Chrome DevTools Protocol (CDP)
description: Create persistent browser sessions, manage tabs, and interact with browsers using Chrome DevTools Protocol (CDP) commands via the /devtools endpoints.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/cdp/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Chrome DevTools Protocol (CDP)

The `/devtools` endpoints provide session management capabilities that follow the [Chrome DevTools Protocol (CDP) ↗](https://chromedevtools.github.io/devtools-protocol/). These endpoints allow you to create persistent browser sessions, manage multiple tabs, and interact with browsers using CDP commands. This is useful for advanced automation, debugging, and remote browser control.

CDP endpoints can be accessed from any environment that supports WebSocket connections, including local development machines, external servers, and CI/CD pipelines. This means you can connect to Browser Run from Node.js scripts, Puppeteer, Playwright, or any CDP-compatible client.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## What is CDP?

The Chrome DevTools Protocol (CDP) is a remote debugging protocol that allows you to instrument, inspect, debug, and profile Chromium-based browsers. It is the same protocol used by Chrome DevTools to control and monitor the browser. Popular browser automation libraries like Puppeteer and Playwright provide high-level APIs over the Chrome DevTools Protocol, making it easier to automate common tasks.

## Use cases

The browser sessions endpoints enable you to:

* **Create and manage persistent browser sessions** — Launch browser instances that remain active for extended periods
* **Open, close, and list browser tabs (targets)** — Manage multiple debuggable targets (pages, iframes, etc.) within a single browser instance
* **Connect via WebSocket to send CDP commands** — Automate browser actions programmatically
* **View live browser sessions using Chrome DevTools UI** — Debug and inspect remote browser sessions visually
* **Integrate with existing CDP clients** — Use standard CDP clients like Puppeteer or custom WebSocket implementations

## How it works

Once you acquire a browser session, you can interact with it in two ways:

### CDP over WebSocket

Connect to the WebSocket endpoint `/devtools/browser` to acquire a session and send [CDP commands ↗](https://chromedevtools.github.io/devtools-protocol/) directly over the connection. This is the standard way to use CDP and works with any CDP client, including [Puppeteer](https://developers.cloudflare.com/browser-run/cdp/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/cdp/playwright/), and [MCP clients](https://developers.cloudflare.com/browser-run/cdp/mcp-clients/).

### HTTP API

HTTP endpoints are also available to manage the browser lifecycle without using WebSockets. These follow the standard [CDP HTTP endpoints ↗](https://chromedevtools.github.io/devtools-protocol/#endpoints):

1. **Create session** — `POST /devtools/browser`
2. **List tabs** — `GET /devtools/browser/{session_id}/json/list`
3. **Create tab** — `PUT /devtools/browser/{session_id}/json/new`
4. **Close tab** — `DELETE /devtools/browser/{session_id}/json/close/{target_id}`
5. **Close session** — `DELETE /devtools/browser/{session_id}`

Check the [API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/) for the full list of endpoints.

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/cdp/","name":"Chrome DevTools Protocol (CDP)"}}]}
```

---

---
title: Using with MCP clients (CDP)
description: Configure AI coding agents to control Browser Run sessions through the Model Context Protocol (MCP) using the chrome-devtools-mcp package.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/cdp/mcp-clients.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Using with MCP clients (CDP)

You can use the CDP endpoints with AI coding agents through the [Model Context Protocol (MCP) ↗](https://modelcontextprotocol.io/). The [chrome-devtools-mcp ↗](https://github.com/ChromeDevTools/chrome-devtools-mcp) package provides an MCP server that allows AI assistants to control and inspect browser sessions.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## What is MCP?

The Model Context Protocol (MCP) is an open protocol that enables AI assistants to interact with external tools and services. By configuring an MCP client with Browser Run, your AI coding agent can perform browser automation tasks like navigating to pages, taking screenshots, running performance audits, and debugging JavaScript.

## Prerequisites

* Node.js v20.19 or newer
* An MCP-compatible AI client (for example, Claude Desktop, Claude Code, Cursor, OpenCode)
* A Browser Run API token with `Browser Rendering - Edit` permissions

## Configure your MCP client

Add the following configuration to your MCP client settings file (the exact location depends on your client):

### Claude Desktop and Claude Code

Add to `claude_desktop_config.json` (Claude Desktop) or `~/.claude.json` (Claude Code):

```

{

  "mcpServers": {

    "browser-rendering": {

      "command": "npx",

      "args": [

        "-y",

        "chrome-devtools-mcp@latest",

        "--wsEndpoint=wss://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-rendering/devtools/browser?keep_alive=600000",

        "--wsHeaders={\"Authorization\":\"Bearer <API_TOKEN>\"}"

      ]

    }

  }

}


```

Explain Code

### OpenCode

Add to `.opencode.jsonc`:

```

{

  "mcp": {

    "browser-rendering": {

      "type": "local",

      "command": [

        "npx",

        "-y",

        "chrome-devtools-mcp@latest",

        "--wsEndpoint=wss://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-rendering/devtools/browser?keep_alive=600000",

        "--wsHeaders={\"Authorization\":\"Bearer <API_TOKEN>\"}"

      ],

      "enabled": true

    }

  }

}


```

Explain Code

### Cursor

Add to `~/.cursor/mcp.json`:

```

{

  "mcpServers": {

    "browser-rendering": {

      "command": "npx",

      "args": [

        "-y",

        "chrome-devtools-mcp@latest",

        "--wsEndpoint=wss://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-rendering/devtools/browser?keep_alive=600000",

        "--wsHeaders={\"Authorization\":\"Bearer <API_TOKEN>\"}"

      ]

    }

  }

}


```

Explain Code

Replace `ACCOUNT_ID` with your Cloudflare account ID and `API_TOKEN` with your Browser Run API token. You can obtain these from your Cloudflare dashboard.

For other MCP clients, refer to the [chrome-devtools-mcp documentation ↗](https://github.com/ChromeDevTools/chrome-devtools-mcp/tree/main?tab=readme-ov-file#mcp-client-configuration).

## Example usage

After configuring the MCP client, you can ask your AI agent to perform browser tasks:

```

Navigate to https://example.com and take a screenshot of the homepage


```

```

Check the console messages on the current page for any errors


```

```

Run a Lighthouse audit on https://developers.cloudflare.com


```

## How it works

The MCP server connects to Browser Run via WebSocket using the CDP protocol:

1. **WebSocket endpoint** \- The `--wsEndpoint` URL connects to the Browser Run service
2. **Authentication** \- The `--wsHeaders` parameter includes your API token for authentication
3. **Keep-alive** \- The `keep_alive` query parameter (in milliseconds) specifies how long the session stays active
4. **MCP protocol** \- The server translates MCP tool calls into CDP commands

Session management

The `--wsEndpoint` parameter creates a new browser session automatically when the MCP server starts. The session remains active for the duration specified in `keep_alive` (in the examples above, 10 minutes). The MCP server will use this session for all browser operations until it is restarted.

## Additional resources

* [chrome-devtools-mcp repository ↗](https://github.com/ChromeDevTools/chrome-devtools-mcp) \- Official MCP server for Chrome DevTools
* [Model Context Protocol documentation ↗](https://modelcontextprotocol.io/) \- Learn more about MCP
* [Claude Desktop MCP setup ↗](https://modelcontextprotocol.io/docs/develop/connect-local-servers) \- Configure MCP servers in Claude Desktop
* [Claude Code MCP setup ↗](https://docs.anthropic.com/en/docs/claude-code/mcp) \- Configure MCP servers in Claude Code
* [Cursor MCP setup ↗](https://cursor.com/docs/mcp) \- Configure MCP servers in Cursor
* [OpenCode MCP setup ↗](https://opencode.ai/docs/mcp-servers/) \- Configure MCP servers in OpenCode

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/cdp/","name":"Chrome DevTools Protocol (CDP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/cdp/mcp-clients/","name":"Using with MCP clients (CDP)"}}]}
```

---

---
title: Using with Playwright (CDP)
description: Connect Playwright to Browser Run sessions from any Node.js environment to automate browser tasks using the Chrome DevTools Protocol.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/cdp/playwright.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Using with Playwright (CDP)

You can use [Playwright ↗](https://playwright.dev/) to connect to Browser Run sessions from any Node.js environment and automate browser tasks programmatically via CDP. This is useful for scripts running on your local machine, CI/CD pipelines, or external servers.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Prerequisites

* Node.js installed on your machine
* A Cloudflare account with Browser Run enabled
* A Browser Run API token with `Browser Rendering - Edit` permissions

## Install Playwright

Install the `playwright-core` package (the version without bundled browsers):

 npm  yarn  pnpm  bun 

```
npm i playwright-core
```

```
yarn add playwright-core
```

```
pnpm add playwright-core
```

```
bun add playwright-core
```

## Connect to Browser Run

The following script demonstrates how to connect to a Browser Run session, navigate to a page, extract the title, and take a screenshot.

Create a file named `script.js`:

JavaScript

```

const { chromium } = require("playwright-core");


const ACCOUNT_ID = process.env.CF_ACCOUNT_ID || "<ACCOUNT_ID>";

const API_TOKEN = process.env.CF_API_TOKEN || "<API_TOKEN>";


const browserWSEndpoint = `wss://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-rendering/devtools/browser?keep_alive=600000`;


async function main() {

  const browser = await chromium.connectOverCDP(browserWSEndpoint, {

    headers: {

      Authorization: `Bearer ${API_TOKEN}`,

    },

  });


  const context = browser.contexts()[0];

  const page = context.pages()[0] || (await context.newPage());

  await page.goto("https://developers.cloudflare.com");


  const title = await page.title();

  console.log(`Page title: ${title}`);


  await page.screenshot({ path: "screenshot.png" });


  await browser.close();

}


main().catch(console.error);


```

Explain Code

Replace `ACCOUNT_ID` with your Cloudflare account ID and `API_TOKEN` with your Browser Run API token, or set them as environment variables:

Terminal window

```

export CF_ACCOUNT_ID="<ACCOUNT_ID>"

export CF_API_TOKEN="<API_TOKEN>"


```

## Run the script

Terminal window

```

node script.js


```

You should see the page title printed to the console and a screenshot saved as `screenshot.png`.

## How it works

The script connects directly to Browser Run via WebSocket using the CDP protocol:

1. **WebSocket endpoint** \- The `browserWSEndpoint` URL acquires a new browser session and connects to it via WebSocket
2. **Authentication** \- The `Authorization` header with your API token authenticates the request
3. **Keep-alive** \- The `keep_alive` parameter (in milliseconds) specifies how long the session stays active
4. **Playwright API** \- Once connected, you use the standard Playwright API to control the browser

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/cdp/","name":"Chrome DevTools Protocol (CDP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/cdp/playwright/","name":"Using with Playwright (CDP)"}}]}
```

---

---
title: Using with Puppeteer (CDP)
description: Connect Puppeteer to Browser Run sessions from any Node.js environment to automate browser tasks using the Chrome DevTools Protocol.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/cdp/puppeteer.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Using with Puppeteer (CDP)

You can use [Puppeteer ↗](https://pptr.dev/) to connect to Browser Run sessions from any Node.js environment and automate browser tasks programmatically via CDP. This is useful for scripts running on your local machine, CI/CD pipelines, or external servers.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

## Prerequisites

* Node.js installed on your machine
* A Cloudflare account with Browser Run enabled
* A Browser Run API token with `Browser Rendering - Edit` permissions

## Install Puppeteer

Install the `puppeteer-core` package (the version without bundled Chrome):

 npm  yarn  pnpm  bun 

```
npm i puppeteer-core
```

```
yarn add puppeteer-core
```

```
pnpm add puppeteer-core
```

```
bun add puppeteer-core
```

## Connect to Browser Run

The following script demonstrates how to connect to a Browser Run session, navigate to a page, extract the title, and take a screenshot.

Create a file named `script.js`:

JavaScript

```

const puppeteer = require("puppeteer-core");


const ACCOUNT_ID = process.env.CF_ACCOUNT_ID || "<ACCOUNT_ID>";

const API_TOKEN = process.env.CF_API_TOKEN || "<API_TOKEN>";


const browserWSEndpoint = `wss://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-rendering/devtools/browser?keep_alive=600000`;


async function main() {

  const browser = await puppeteer.connect({

    browserWSEndpoint,

    headers: {

      Authorization: `Bearer ${API_TOKEN}`,

    },

  });


  const page = await browser.newPage();

  await page.goto("https://developers.cloudflare.com");


  const title = await page.title();

  console.log(`Page title: ${title}`);


  await page.screenshot({ path: "screenshot.png" });


  await browser.close();

}


main().catch(console.error);


```

Explain Code

Replace `ACCOUNT_ID` with your Cloudflare account ID and `API_TOKEN` with your Browser Run API token, or set them as environment variables:

Terminal window

```

export CF_ACCOUNT_ID="<ACCOUNT_ID>"

export CF_API_TOKEN="<API_TOKEN>"


```

## Run the script

Terminal window

```

node script.js


```

You should see the page title printed to the console and a screenshot saved as `screenshot.png`.

## How it works

The script connects directly to Browser Run via WebSocket using the CDP protocol:

1. **WebSocket endpoint** \- The `browserWSEndpoint` URL acquires a new browser session and connects to it via WebSocket
2. **Authentication** \- The `Authorization` header with your API token authenticates the request
3. **Keep-alive** \- The `keep_alive` parameter (in milliseconds) specifies how long the session stays active
4. **Puppeteer API** \- Once connected, you use the standard Puppeteer API to control the browser

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/cdp/","name":"Chrome DevTools Protocol (CDP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/cdp/puppeteer/","name":"Using with Puppeteer (CDP)"}}]}
```

---

---
title: Session management (HTTP)
description: Manage browser sessions and tabs using HTTP endpoints, including creating sessions, listing targets, and opening the Chrome DevTools UI.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/cdp/session-management.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Session management (HTTP)

Use the HTTP API to manage browser sessions and tabs without using WebSocket connections. This is useful for session lifecycle operations like creating sessions, listing tabs, and cleaning up resources.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with `Browser Rendering - Edit` permission. For more information, refer to [Quick Actions — Before you begin](https://developers.cloudflare.com/browser-run/quick-actions/#before-you-begin).

The [API reference](https://developers.cloudflare.com/api/resources/browser%5Frendering/) documents all session management endpoints under `/devtools`.

## Step 1: Acquire a browser session

Create a new browser session using the `POST /devtools/browser` endpoint. The session will remain active for the specified keep-alive time (in this example, 10 minutes).

Terminal window

```

curl "https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/browser-rendering/devtools/browser?keep_alive=600000" \

  --request POST \

  --header "Authorization: Bearer {api_token}"


```

```

{

  "sessionId": "1909cef7-23e8-4394-bc31-27404bf4348f",

  "webSocketDebuggerUrl": "wss://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/devtools/browser/1909cef7-23e8-4394-bc31-27404bf4348f"

}


```

Save the `sessionId` from the response. You will use it in subsequent requests.

## Step 2: Create a tab with a specific URL

Open a new tab in your browser session and navigate to a specific URL using the `PUT /devtools/browser/{session_id}/json/new` endpoint.

Terminal window

```

curl "https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/browser-rendering/devtools/browser/SESSION_ID/json/new?url=https%3A%2F%2Fexample.com" \

  --request PUT \

  --header "Authorization: Bearer {api_token}"


```

```

{

  "id": "8E598E996530FB09E46A22B8B7754F7F",

  "type": "page",

  "url": "https://example.com",

  "title": "Example Domain",

  "description": "",

  "devtoolsFrontendUrl": "https://live.browser.run/ui/view?wss=live.browser.run/api/devtools/browser/1909cef7-23e8-4394-bc31-27404bf4348f/page/8E598E996530FB09E46A22B8B7754F7F?jwt=...",

  "webSocketDebuggerUrl": "wss://live.browser.run/api/devtools/browser/1909cef7-23e8-4394-bc31-27404bf4348f/page/8E598E996530FB09E46A22B8B7754F7F?jwt=..."

}


```

## Step 3: List all targets

List all targets (tabs) in your session to verify the tab was created and get the `devtoolsFrontendUrl`.

Terminal window

```

curl "https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/browser-rendering/devtools/browser/SESSION_ID/json/list" \

  --request GET \

  --header "Authorization: Bearer {api_token}"


```

```

[

  {

    "id": "8E598E996530FB09E46A22B8B7754F7F",

    "type": "page",

    "url": "https://example.com",

    "title": "Example Domain",

    "description": "",

    "devtoolsFrontendUrl": "https://live.browser.run/ui/view?wss=live.browser.run/api/devtools/browser/1909cef7-23e8-4394-bc31-27404bf4348f/page/8E598E996530FB09E46A22B8B7754F7F?jwt=...",

    "webSocketDebuggerUrl": "wss://live.browser.run/api/devtools/browser/1909cef7-23e8-4394-bc31-27404bf4348f/page/8E598E996530FB09E46A22B8B7754F7F?jwt=..."

  }

]


```

Explain Code

## Step 4: Open the DevTools UI

Copy the `devtoolsFrontendUrl` from the response and open it in Chrome. This URL provides direct access to the Chrome DevTools UI connected to your remote browser session.

URL validity

The `devtoolsFrontendUrl` is valid for five minutes from when it was generated. If you do not open the URL within this timeframe, it will expire and you will need to list the targets again to get a fresh URL. Once the DevTools connection is established, it remains active as long as the browser session is alive.

Once opened, the DevTools UI will load and you can:

* Inspect the DOM and CSS
* Debug JavaScript with breakpoints
* Monitor network requests
* View console messages
* Execute JavaScript in the console
* Navigate to different URLs

## Step 5: Clean up

When you are done, close the browser session to release resources.

Terminal window

```

curl "https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/browser-rendering/devtools/browser/SESSION_ID" \

  --request DELETE \

  --header "Authorization: Bearer {api_token}"


```

```

{

  "status": "closing"

}


```

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/cdp/","name":"Chrome DevTools Protocol (CDP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/cdp/session-management/","name":"Session management (HTTP)"}}]}
```

---

---
title: Playwright
description: Learn how to use Playwright with Cloudflare Workers for browser automation. Access Playwright API, manage sessions, and optimize Browser Run.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/playwright/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Playwright

[Playwright ↗](https://playwright.dev/) is an open-source package developed by Microsoft that can do browser automation tasks; it is commonly used to write frontend tests, create screenshots, or crawl pages.

The Workers team forked a [version of Playwright ↗](https://github.com/cloudflare/playwright) that was modified to be compatible with [Cloudflare Workers](https://developers.cloudflare.com/workers/) and [Browser Run](https://developers.cloudflare.com/browser-run/).

Our version is open sourced and can be found in [Cloudflare's fork of Playwright ↗](https://github.com/cloudflare/playwright). The npm package can be installed from [npmjs ↗](https://www.npmjs.com/) as [@cloudflare/playwright ↗](https://www.npmjs.com/package/@cloudflare/playwright):

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/playwright
```

```
yarn add -D @cloudflare/playwright
```

```
pnpm add -D @cloudflare/playwright
```

```
bun add -d @cloudflare/playwright
```

Note

The current version is [@cloudflare/playwright v1.3.0 ↗](https://github.com/cloudflare/playwright/releases/tag/v1.3.0), based on [Playwright v1.58.2 ↗](https://playwright.dev/docs/release-notes#version-158).

## Use Playwright in a Worker

In this [example ↗](https://github.com/cloudflare/playwright/tree/main/packages/playwright-cloudflare/examples/todomvc), you will run Playwright tests in a Cloudflare Worker using the [todomvc ↗](https://demo.playwright.dev/todomvc) application.

If you want to skip the steps and get started quickly, select **Deploy to Cloudflare** below.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/playwright/tree/main/packages/playwright-cloudflare/examples/todomvc)

Make sure you have the [browser binding](https://developers.cloudflare.com/browser-run/reference/wrangler/#bindings) configured in your Wrangler configuration file:

Note

To use the latest version of `@cloudflare/playwright`, your Worker configuration must include the `nodejs_compat` compatibility flag and a `compatibility_date` of 2025-09-15 or later. This change is necessary because the library's functionality requires the native `node.fs` API.

* [  wrangler.jsonc ](#tab-panel-3572)
* [  wrangler.toml ](#tab-panel-3573)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "cloudflare-playwright-example",

  "main": "src/index.ts",

  "workers_dev": true,

  "compatibility_flags": ["nodejs_compat"],

  // Set this to today's date

  "compatibility_date": "2026-04-16",

  "upload_source_maps": true,

  "browser": {

    "binding": "MYBROWSER",

  },

}


```

Explain Code

TOML

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "cloudflare-playwright-example"

main = "src/index.ts"

workers_dev = true

compatibility_flags = [ "nodejs_compat" ]

# Set this to today's date

compatibility_date = "2026-04-16"

upload_source_maps = true


[browser]

binding = "MYBROWSER"


```

Explain Code

Install the npm package:

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/playwright
```

```
yarn add -D @cloudflare/playwright
```

```
pnpm add -D @cloudflare/playwright
```

```
bun add -d @cloudflare/playwright
```

Let's look at some examples of how to use Playwright:

### Take a screenshot

Using browser automation to take screenshots of web pages is a common use case. This script tells the browser to navigate to [https://demo.playwright.dev/todomvc ↗](https://demo.playwright.dev/todomvc), create some items, take a screenshot of the page, and return the image in the response.

TypeScript

```

import { launch } from "@cloudflare/playwright";


export default {

  async fetch(request: Request, env: Env) {

    const browser = await launch(env.MYBROWSER);

    const page = await browser.newPage();


    await page.goto("https://demo.playwright.dev/todomvc");


    const TODO_ITEMS = [

      "buy some cheese",

      "feed the cat",

      "book a doctors appointment",

    ];


    const newTodo = page.getByPlaceholder("What needs to be done?");

    for (const item of TODO_ITEMS) {

      await newTodo.fill(item);

      await newTodo.press("Enter");

    }


    const img = await page.screenshot();

    await browser.close();


    return new Response(img, {

      headers: {

        "Content-Type": "image/png",

      },

    });

  },

};


```

Explain Code

### Trace

A Playwright trace is a detailed log of your workflow execution that captures information like user clicks and navigation actions, screenshots of the page, and any console messages generated and used for debugging. Developers can take a `trace.zip` file and either open it [locally ↗](https://playwright.dev/docs/trace-viewer#opening-the-trace) or upload it to the [Playwright Trace Viewer ↗](https://trace.playwright.dev/), a GUI tool that helps you explore the data.

Here's an example of a worker generating a trace file:

TypeScript

```

import fs from "fs";

import { launch } from "@cloudflare/playwright";


export default {

  async fetch(request: Request, env: Env) {

    const browser = await launch(env.MYBROWSER);

    const page = await browser.newPage();


    // Start tracing before navigating to the page

    await page.context().tracing.start({ screenshots: true, snapshots: true });


    await page.goto("https://demo.playwright.dev/todomvc");


    const TODO_ITEMS = [

      "buy some cheese",

      "feed the cat",

      "book a doctors appointment",

    ];


    const newTodo = page.getByPlaceholder("What needs to be done?");

    for (const item of TODO_ITEMS) {

      await newTodo.fill(item);

      await newTodo.press("Enter");

    }


    // Stop tracing and save the trace to a zip file

    await page.context().tracing.stop({ path: "trace.zip" });

    await browser.close();

    const file = await fs.promises.readFile("trace.zip");


    return new Response(file, {

      status: 200,

      headers: {

        "Content-Type": "application/zip",

      },

    });

  },

};


```

Explain Code

### Assertions

One of the most common use cases for using Playwright is software testing. Playwright includes test assertion features in its APIs; refer to [Assertions ↗](https://playwright.dev/docs/test-assertions) in the Playwright documentation for details. Here's an example of a Worker doing `expect()` test assertions of the [todomvc ↗](https://demo.playwright.dev/todomvc) demo page:

TypeScript

```

import { launch } from "@cloudflare/playwright";

import { expect } from "@cloudflare/playwright/test";


export default {

  async fetch(request: Request, env: Env) {

    const browser = await launch(env.MYBROWSER);

    const page = await browser.newPage();


    await page.goto("https://demo.playwright.dev/todomvc");


    const TODO_ITEMS = [

      "buy some cheese",

      "feed the cat",

      "book a doctors appointment",

    ];


    const newTodo = page.getByPlaceholder("What needs to be done?");

    for (const item of TODO_ITEMS) {

      await newTodo.fill(item);

      await newTodo.press("Enter");

    }


    await expect(page.getByTestId("todo-title")).toHaveCount(TODO_ITEMS.length);


    await Promise.all(

      TODO_ITEMS.map((value, index) =>

        expect(page.getByTestId("todo-title").nth(index)).toHaveText(value),

      ),

    );

  },

};


```

Explain Code

### Storage state

Playwright supports [storage state ↗](https://playwright.dev/docs/api/class-browsercontext#browsercontext-storage-state) to obtain and persist cookies and other storage data. In this example, you will use storage state to persist cookies and other storage data in [Workers KV](https://developers.cloudflare.com/kv).

First, ensure you have a KV namespace. You can create a new one with:

Terminal window

```

npx wrangler kv namespace create KV


```

Then, add the KV namespace to your Wrangler configuration file:

* [  wrangler.jsonc ](#tab-panel-3574)
* [  wrangler.toml ](#tab-panel-3575)

JSONC

```

{

  "name": "storage-state-examples",

  "main": "src/index.ts",

  "compatibility_flags": ["nodejs_compat"],

  // Set this to today's date

  "compatibility_date": "2026-04-16",

  "browser": {

    "binding": "MYBROWSER",

  },

  "kv_namespaces": [

    {

      "binding": "KV",

      "id": "<YOUR-KV-NAMESPACE-ID>",

    },

  ],

}


```

Explain Code

TOML

```

name = "storage-state-examples"

main = "src/index.ts"

compatibility_flags = [ "nodejs_compat" ]

# Set this to today's date

compatibility_date = "2026-04-16"


[browser]

binding = "MYBROWSER"


[[kv_namespaces]]

binding = "KV"

id = "<YOUR-KV-NAMESPACE-ID>"


```

Explain Code

Now, you can use the storage state to persist cookies and other storage data in KV:

src/index.ts

```

// gets persisted storage state from KV or undefined if it does not exist

const storageStateJson = await env.KV.get("storageState");

const storageState = storageStateJson

  ? ((await JSON.parse(

      storageStateJson,

    )) as BrowserContextOptions["storageState"])

  : undefined;


await using browser = await launch(env.MYBROWSER);

// creates a new context with storage state persisted in KV

await using context = await browser.newContext({ storageState });


await using page = await context.newPage();


// do some actions on the page that may update client-side storage


// gets updated storage state: cookies, localStorage, and IndexedDB

const updatedStorageState = await context.storageState({ indexedDB: true });


// persists updated storage state in KV

await env.KV.put("storageState", JSON.stringify(updatedStorageState));


```

Explain Code

### Keep Alive

If users omit the `browser.close()` statement, the browser instance will stay open, ready to be connected to again and [re-used](https://developers.cloudflare.com/browser-run/features/reuse-sessions/) but it will, by default, close automatically after 1 minute of inactivity. Users can optionally extend this idle time up to 10 minutes, by using the `keep_alive` option, set in milliseconds:

JavaScript

```

const browser = await playwright.launch(env.MYBROWSER, { keep_alive: 600000 });


```

Using the above, the browser will stay open for up to 10 minutes, even if inactive.

Note

This is an inactivity timeout, not a maximum session duration. Sessions can remain open longer than 10 minutes as long as they stay active. To keep a session open beyond the inactivity timeout, send a command at least once within your configured window (for example, every 10 minutes). Refer to [session duration limits](https://developers.cloudflare.com/browser-run/limits/#is-there-a-maximum-session-duration) for more information.

### Session Reuse

The best way to improve the performance of your Browser Run Worker is to reuse sessions by keeping the browser open after you have finished with it, and connecting to that session each time you have a new request. Playwright handles [browser.close ↗](https://playwright.dev/docs/api/class-browser#browser-close) differently from Puppeteer. In Playwright, if the browser was obtained using a `connect` session, the session will disconnect. If the browser was obtained using a `launch` session, the session will close.

JavaScript

```

import { env } from "cloudflare:workers";

import { acquire, connect } from "@cloudflare/playwright";


async function reuseSameSession() {

  // acquires a new session

  const { sessionId } = await acquire(env.BROWSER);


  for (let i = 0; i < 5; i++) {

    // connects to the session that was previously acquired

    const browser = await connect(env.BROWSER, sessionId);


    // ...


    // this will disconnect the browser from the session, but the session will be kept alive

    await browser.close();

  }

}


```

Explain Code

### Set a custom user agent

To specify a custom user agent in Playwright, set it in the options when creating a new browser context with `browser.newContext()`. All pages subsequently created from this context will use the new user agent. This is useful if the target website serves different content based on the user agent.

JavaScript

```

const context = await browser.newContext({

  userAgent:

    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",

});


```

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot.

## Local debugging with headful mode (experimental)

When developing locally with `wrangler dev` or `vite dev`, Chrome runs in headless mode by default. To launch Chrome in visible (headful) mode, set the `X_BROWSER_HEADFUL` environment variable:

Terminal window

```

X_BROWSER_HEADFUL=true npx wrangler dev


```

Or with the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/):

Terminal window

```

X_BROWSER_HEADFUL=true npx vite dev


```

This opens a browser window so you can watch your Playwright automation in real time, making it easier to debug navigation, element selection, and page interactions.

Note

When using `@cloudflare/playwright` in headful mode, two Chrome windows may appear. This is expected behavior due to how Playwright handles browser contexts via CDP.

## Session management

In order to facilitate browser session management, we have extended the Playwright API with new methods:

### List open sessions

`playwright.sessions()` lists the current running sessions. It will return an output similar to this:

```

[

  {

    "connectionId": "2a2246fa-e234-4dc1-8433-87e6cee80145",

    "connectionStartTime": 1711621704607,

    "sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",

    "startTime": 1711621703708

  },

  {

    "sessionId": "565e05fb-4d2a-402b-869b-5b65b1381db7",

    "startTime": 1711621703808

  }

]


```

Explain Code

Notice that the session `478f4d7d-e943-40f6-a414-837d3736a1dc` has an active worker connection (`connectionId=2a2246fa-e234-4dc1-8433-87e6cee80145`), while session `565e05fb-4d2a-402b-869b-5b65b1381db7` is free. While a connection is active, no other workers may connect to that session.

### List recent sessions

`playwright.history()` lists recent sessions, both open and closed. It is useful to get a sense of your current usage.

```

[

  {

    "closeReason": 2,

    "closeReasonText": "BrowserIdle",

    "endTime": 1711621769485,

    "sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",

    "startTime": 1711621703708

  },

  {

    "closeReason": 1,

    "closeReasonText": "NormalClosure",

    "endTime": 1711123501771,

    "sessionId": "2be00a21-9fb6-4bb2-9861-8cd48e40e771",

    "startTime": 1711123430918

  }

]


```

Explain Code

Session `2be00a21-9fb6-4bb2-9861-8cd48e40e771` was closed explicitly with `browser.close()` by the client, while session `478f4d7d-e943-40f6-a414-837d3736a1dc` was closed due to reaching the maximum idle time (check [limits](https://developers.cloudflare.com/browser-run/limits/)).

You should also be able to access this information in the dashboard, albeit with a slight delay.

### Active limits

`playwright.limits()` lists your active limits:

```

{

  "activeSessions": [

    { "id": "478f4d7d-e943-40f6-a414-837d3736a1dc" },

    { "id": "565e05fb-4d2a-402b-869b-5b65b1381db7" }

  ],

  "allowedBrowserAcquisitions": 1,

  "maxConcurrentSessions": 2,

  "timeUntilNextAllowedBrowserAcquisition": 0

}


```

* `activeSessions` lists the IDs of the current open sessions.
* `maxConcurrentSessions` defines how many browsers can be open at the same time.
* `allowedBrowserAcquisitions` specifies if a new browser session can be opened according to the rate [limits](https://developers.cloudflare.com/browser-run/limits/) in place.
* `timeUntilNextAllowedBrowserAcquisition` defines the waiting period before a new browser can be launched.

## Playwright API

The full Playwright API can be found at the [Playwright API documentation ↗](https://playwright.dev/docs/api/class-playwright).

The following capabilities are not yet fully supported, but we’re actively working on them:

* [Playwright Test ↗](https://playwright.dev/docs/test-configuration) except [Assertions ↗](https://playwright.dev/docs/test-assertions)
* [Components ↗](https://playwright.dev/docs/test-components)
* [Firefox ↗](https://playwright.dev/docs/api/class-playwright#playwright-firefox), [Android ↗](https://playwright.dev/docs/api/class-android) and [Electron ↗](https://playwright.dev/docs/api/class-electron), as well as different versions of Chrome
* [Videos ↗](https://playwright.dev/docs/next/videos)

This is **not an exhaustive list** — expect rapid changes as we work toward broader parity with the original feature set. You can also check [latest test results ↗](https://playwright-full-test-report.pages.dev/) for a granular up to date list of the features that are fully supported.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/playwright/","name":"Playwright"}}]}
```

---

---
title: Playwright MCP
description: Deploy a Playwright MCP server that uses Browser Run to provide browser automation capabilities to your agents.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/playwright/playwright-mcp.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Playwright MCP

[@cloudflare/playwright-mcp ↗](https://github.com/cloudflare/playwright-mcp) is a [Playwright MCP ↗](https://github.com/microsoft/playwright-mcp) server fork that provides browser automation capabilities using Playwright and Browser Run.

This server enables LLMs to interact with web pages through structured accessibility snapshots, bypassing the need for screenshots or visually-tuned models. Its key features are:

* Fast and lightweight. Uses Playwright's accessibility tree, not pixel-based input.
* LLM-friendly. No vision models needed, operates purely on structured data.
* Deterministic tool application. Avoids ambiguity common with screenshot-based approaches.

Note

The current version of Cloudflare Playwright MCP [v1.1.1 ↗](https://github.com/cloudflare/playwright/releases/tag/v1.1.1) is in sync with upstream Playwright MCP [v0.0.30 ↗](https://github.com/microsoft/playwright-mcp/releases/tag/v0.0.30).

## Quick start

If you are already familiar with Cloudflare Workers and you want to get started with Playwright MCP right away, select this button:

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/playwright-mcp/tree/main/cloudflare/example)

This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. Use this option if you are familiar with Cloudflare Workers, and wish to skip the step-by-step guidance.

Check our [GitHub page ↗](https://github.com/cloudflare/playwright-mcp) for more information on how to build and deploy Playwright MCP.

## Deploying

Follow these steps to deploy `@cloudflare/playwright-mcp`:

1. Install the Playwright MCP [npm package ↗](https://www.npmjs.com/package/@cloudflare/playwright-mcp).

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/playwright-mcp
```

```
yarn add -D @cloudflare/playwright-mcp
```

```
pnpm add -D @cloudflare/playwright-mcp
```

```
bun add -d @cloudflare/playwright-mcp
```

1. Make sure you have the [Browser Run](https://developers.cloudflare.com/browser-run/) and [Durable Object](https://developers.cloudflare.com/durable-objects/) bindings and [migrations](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/) in your Wrangler configuration file.

Note

Your Worker configuration must include the `nodejs_compat` compatibility flag and a `compatibility_date` of 2025-09-15 or later.

* [  wrangler.jsonc ](#tab-panel-3576)
* [  wrangler.toml ](#tab-panel-3577)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "playwright-mcp-example",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-16",

  "compatibility_flags": ["nodejs_compat"],

  "browser": {

    "binding": "BROWSER",

  },

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["PlaywrightMCP"],

    },

  ],

  "durable_objects": {

    "bindings": [

      {

        "name": "MCP_OBJECT",

        "class_name": "PlaywrightMCP",

      },

    ],

  },

}


```

Explain Code

TOML

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "playwright-mcp-example"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-16"

compatibility_flags = [ "nodejs_compat" ]


[browser]

binding = "BROWSER"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "PlaywrightMCP" ]


[[durable_objects.bindings]]

name = "MCP_OBJECT"

class_name = "PlaywrightMCP"


```

Explain Code

1. Edit the code.

src/index.ts

```

import { env } from "cloudflare:workers";

import { createMcpAgent } from "@cloudflare/playwright-mcp";


export const PlaywrightMCP = createMcpAgent(env.BROWSER);


export default {

  fetch(request: Request, env: Env, ctx: ExecutionContext) {

    const { pathname } = new URL(request.url);


    switch (pathname) {

      case "/sse":

      case "/sse/message":

        return PlaywrightMCP.serveSSE("/sse").fetch(request, env, ctx);

      case "/mcp":

        return PlaywrightMCP.serve("/mcp").fetch(request, env, ctx);

      default:

        return new Response("Not Found", { status: 404 });

    }

  },

};


```

Explain Code

1. Deploy the server.

Terminal window

```

npx wrangler deploy


```

The server is now available at `https://[my-mcp-url].workers.dev/sse` and you can use it with any MCP client.

## Using Playwright MCP

![Screenshot of the AI Playground](https://developers.cloudflare.com/_astro/playground-ai-screenshot.v44jFMBu_Z1xgc6e.webp) 

[Cloudflare AI Playground ↗](https://playground.ai.cloudflare.com/) is a great way to test MCP servers using LLM models available in Workers AI.

1. Go to [https://playground.ai.cloudflare.com/ ↗](https://playground.ai.cloudflare.com/).
2. Ensure that the model is set to `llama-3.3-70b-instruct-fp8-fast`.
3. In **MCP Servers**, set **URL** to `https://[my-mcp-url].workers.dev/sse`.
4. Click **Connect**.
5. Status should update to **Connected** and it should list 23 available tools.

You can now start to interact with the model, and it will run necessary the tools to accomplish what was requested.

Note

For best results, give simple instructions consisting of one single action. For example, "Create a new todo entry", "Go to cloudflare site", "Take a screenshot".

Try this sequence of instructions to see Playwright MCP in action:

1. "Go to demo.playwright.dev/todomvc"
2. "Create some todo entry"
3. "Nice. Now create a todo in parrot style"
4. "And create another todo in Yoda style"
5. "Take a screenshot"

You can also use other MCP clients like [Claude Desktop ↗](https://github.com/cloudflare/playwright-mcp/blob/main/cloudflare/example/README.md#use-with-claude-desktop).

Check our [GitHub page ↗](https://github.com/cloudflare/playwright-mcp) for more examples and MCP client configuration options, and refer to the developer documentation on how to [build Agents on Cloudflare](https://developers.cloudflare.com/agents/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/playwright/","name":"Playwright"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/playwright/playwright-mcp/","name":"Playwright MCP"}}]}
```

---

---
title: Puppeteer
description: Learn how to use Puppeteer with Cloudflare Workers for browser automation. Access Puppeteer API, manage sessions, and optimize Browser Run.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/puppeteer.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Puppeteer

[Puppeteer ↗](https://pptr.dev/) is one of the most popular libraries that abstract the lower-level DevTools protocol from developers and provides a high-level API that you can use to easily instrument Chrome/Chromium and automate browsing sessions. Puppeteer is used for tasks like creating screenshots, crawling pages, and testing web applications.

Puppeteer typically connects to a local Chrome or Chromium browser using the DevTools port. Refer to the [Puppeteer API documentation on the Puppeteer.connect() method ↗](https://pptr.dev/api/puppeteer.puppeteer.connect) for more information.

The Workers team forked a version of Puppeteer and patched it to connect to the Workers Browser Run API instead. After connecting, the developers can then use the full [Puppeteer API ↗](https://github.com/cloudflare/puppeteer/blob/main/docs/api/index.md) as they would on a standard setup.

Our version is open sourced and can be found in [Cloudflare's fork of Puppeteer ↗](https://github.com/cloudflare/puppeteer). The npm can be installed from [npmjs ↗](https://www.npmjs.com/) as [@cloudflare/puppeteer ↗](https://www.npmjs.com/package/@cloudflare/puppeteer):

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/puppeteer
```

```
yarn add -D @cloudflare/puppeteer
```

```
pnpm add -D @cloudflare/puppeteer
```

```
bun add -d @cloudflare/puppeteer
```

Note

The current version is [@cloudflare/puppeteer v1.1.0 ↗](https://github.com/cloudflare/puppeteer/releases/tag/v1.1.0), based on [Puppeteer v22.13.1 ↗](https://pptr.dev/chromium-support).

## Use Puppeteer in a Worker

Once the [browser binding](https://developers.cloudflare.com/browser-run/reference/wrangler/#bindings) is configured and the `@cloudflare/puppeteer` library is installed, Puppeteer can be used in a Worker:

* [  JavaScript ](#tab-panel-3578)
* [  TypeScript ](#tab-panel-3579)

JavaScript

```

import puppeteer from "@cloudflare/puppeteer";


export default {

  async fetch(request, env) {

    const browser = await puppeteer.launch(env.MYBROWSER);

    const page = await browser.newPage();

    await page.goto("https://example.com");

    const metrics = await page.metrics();

    await browser.close();

    return Response.json(metrics);

  },

};


```

Explain Code

TypeScript

```

import puppeteer from "@cloudflare/puppeteer";


interface Env {

  MYBROWSER: Fetcher;

}


export default {

  async fetch(request, env): Promise<Response> {

    const browser = await puppeteer.launch(env.MYBROWSER);

    const page = await browser.newPage();

    await page.goto("https://example.com");

    const metrics = await page.metrics();

    await browser.close();

    return Response.json(metrics);

  },

} satisfies ExportedHandler<Env>;


```

Explain Code

This script [launches ↗](https://pptr.dev/api/puppeteer.puppeteernode.launch) the `env.MYBROWSER` browser, opens a [new page ↗](https://pptr.dev/api/puppeteer.browser.newpage), [goes to ↗](https://pptr.dev/api/puppeteer.page.goto) [https://example.com/ ↗](https://example.com/), gets the page load [metrics ↗](https://pptr.dev/api/puppeteer.page.metrics), [closes ↗](https://pptr.dev/api/puppeteer.browser.close) the browser and prints metrics in JSON.

### Keep Alive

If users omit the `browser.close()` statement, it will stay open, ready to be connected to again and [re-used](https://developers.cloudflare.com/browser-run/features/reuse-sessions/) but it will, by default, close automatically after 1 minute of inactivity. Users can optionally extend this idle time up to 10 minutes, by using the `keep_alive` option, set in milliseconds:

JavaScript

```

const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });


```

Using the above, the browser will stay open for up to 10 minutes, even if inactive.

Note

This is an inactivity timeout, not a maximum session duration. Sessions can remain open longer than 10 minutes as long as they stay active. To keep a session open beyond the inactivity timeout, send a command at least once within your configured window (for example, every 10 minutes). Refer to [session duration limits](https://developers.cloudflare.com/browser-run/limits/#is-there-a-maximum-session-duration) for more information.

### Set a custom user agent

To specify a custom user agent in Puppeteer, use the `page.setUserAgent()` method. This is useful if the target website serves different content based on the user agent.

JavaScript

```

await page.setUserAgent(

  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",

);


```

Note

The `userAgent` parameter does not bypass bot protection. Requests from Browser Run will always be identified as a bot.

## Local debugging with headful mode (experimental)

When developing locally with `wrangler dev` or `vite dev`, Chrome runs in headless mode by default. To launch Chrome in visible (headful) mode, set the `X_BROWSER_HEADFUL` environment variable:

Terminal window

```

X_BROWSER_HEADFUL=true npx wrangler dev


```

Or with the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/):

Terminal window

```

X_BROWSER_HEADFUL=true npx vite dev


```

This opens a browser window so you can watch your Puppeteer automation in real time, making it easier to debug navigation, element selection, and page interactions.

## Element selection

Puppeteer provides multiple methods for selecting elements on a page. While CSS selectors work as expected, XPath selectors are not supported due to security constraints in the Workers runtime.

Instead of using Xpath selectors, you can use CSS selectors or `page.evaluate()` to run XPath queries in the browser context:

TypeScript

```

const innerHtml = await page.evaluate(() => {

  return (

    // @ts-ignore this runs on browser context

    new XPathEvaluator()

      .createExpression("/html/body/div/h1")

      // @ts-ignore this runs on browser context

      .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue

      .innerHTML

  );

});


```

Explain Code

Note

`page.evaluate()` can only return primitive types like strings, numbers, and booleans. Returning complex objects like `HTMLElement` will not work.

## Session management

In order to facilitate browser session management, we've added new methods to `puppeteer`:

### List open sessions

`puppeteer.sessions()` lists the current running sessions. It will return an output similar to this:

```

[

  {

    "connectionId": "2a2246fa-e234-4dc1-8433-87e6cee80145",

    "connectionStartTime": 1711621704607,

    "sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",

    "startTime": 1711621703708

  },

  {

    "sessionId": "565e05fb-4d2a-402b-869b-5b65b1381db7",

    "startTime": 1711621703808

  }

]


```

Explain Code

Notice that the session `478f4d7d-e943-40f6-a414-837d3736a1dc` has an active worker connection (`connectionId=2a2246fa-e234-4dc1-8433-87e6cee80145`), while session `565e05fb-4d2a-402b-869b-5b65b1381db7` is free. While a connection is active, no other workers may connect to that session.

### List recent sessions

`puppeteer.history()` lists recent sessions, both open and closed. It's useful to get a sense of your current usage.

```

[

  {

    "closeReason": 2,

    "closeReasonText": "BrowserIdle",

    "endTime": 1711621769485,

    "sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",

    "startTime": 1711621703708

  },

  {

    "closeReason": 1,

    "closeReasonText": "NormalClosure",

    "endTime": 1711123501771,

    "sessionId": "2be00a21-9fb6-4bb2-9861-8cd48e40e771",

    "startTime": 1711123430918

  }

]


```

Explain Code

Session `2be00a21-9fb6-4bb2-9861-8cd48e40e771` was closed explicitly with `browser.close()` by the client, while session `478f4d7d-e943-40f6-a414-837d3736a1dc` was closed due to reaching the maximum idle time (check [limits](https://developers.cloudflare.com/browser-run/limits/)).

You should also be able to access this information in the dashboard, albeit with a slight delay.

### Active limits

`puppeteer.limits()` lists your active limits:

```

{

  "activeSessions": [

    { "id": "478f4d7d-e943-40f6-a414-837d3736a1dc" },

    { "id": "565e05fb-4d2a-402b-869b-5b65b1381db7" }

  ],

  "allowedBrowserAcquisitions": 1,

  "maxConcurrentSessions": 2,

  "timeUntilNextAllowedBrowserAcquisition": 0

}


```

* `activeSessions` lists the IDs of the current open sessions
* `maxConcurrentSessions` defines how many browsers can be open at the same time
* `allowedBrowserAcquisitions` specifies if a new browser session can be opened according to the rate [limits](https://developers.cloudflare.com/browser-run/limits/) in place
* `timeUntilNextAllowedBrowserAcquisition` defines the waiting period before a new browser can be launched.

## Puppeteer API

The full Puppeteer API can be found in the [Cloudflare's fork of Puppeteer ↗](https://github.com/cloudflare/puppeteer/blob/main/docs/api/index.md).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/puppeteer/","name":"Puppeteer"}}]}
```

---

---
title: Stagehand
description: Deploy a Stagehand server that uses Browser Run to provide browser automation capabilities to your agents.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/stagehand.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Stagehand

[Stagehand ↗](https://www.stagehand.dev/) is an open-source, AI-powered browser automation library. Stagehand lets you combine code with natural-language instructions powered by AI, eliminating the need to dictate exact steps or specify selectors. With Stagehand, your agents are more resilient to website changes and easier to maintain, helping you build more reliably and flexibly.

This guide shows you how to deploy a [Worker](https://developers.cloudflare.com/workers/) that uses Stagehand, Browser Run, and [Workers AI](https://developers.cloudflare.com/workers-ai/) to automate a web task.

Note

Browser Run currently supports `@browserbasehq/stagehand` `v2.5.x` only. Stagehand `v3` and later are not supported because they are not Playwright-based.

## Use Stagehand in a Worker with Workers AI

In this example, you will use Stagehand to search for a movie on this [example movie directory ↗](https://demo.playwright.dev/movies), extract its details (title, year, rating, duration, and genre), and return the information along with a screenshot of the webpage.

See a video of this example

![Stagehand video](https://developers.cloudflare.com/images/browser-run/speedystagehand.gif)

Output:

![Stagehand example result](https://developers.cloudflare.com/_astro/stagehand-example.CsX-7-FC_ZvBkPq.webp)

If instead you want to skip the steps and get started right away, select **Deploy to Cloudflare** below.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/playwright/tree/main/packages/playwright-cloudflare/examples/stagehand)

After you deploy, you can interact with the Worker using this URL pattern:

```

https://<your-worker>.workers.dev


```

### 1\. Set up your project

Install the necessary dependencies:

Terminal window

```

npm ci


```

### 2\. Configure your Worker

Update your Wrangler configuration file to include the bindings for Browser Run and [Workers AI](https://developers.cloudflare.com/workers-ai/):

Note

Your Worker configuration must include the `nodejs_compat` compatibility flag and a `compatibility_date` of 2025-09-15 or later.

* [  wrangler.jsonc ](#tab-panel-3610)
* [  wrangler.toml ](#tab-panel-3611)

JSONC

```

{

  "name": "stagehand-example",

  "main": "src/index.ts",

  "compatibility_flags": ["nodejs_compat"],

  // Set this to today's date

  "compatibility_date": "2026-04-16",

  "observability": {

    "enabled": true

  },

  "browser": {

    "binding": "BROWSER"

  },

  "ai": {

    "binding": "AI"

  }

}


```

Explain Code

TOML

```

name = "stagehand-example"

main = "src/index.ts"

compatibility_flags = [ "nodejs_compat" ]

# Set this to today's date

compatibility_date = "2026-04-16"


[observability]

enabled = true


[browser]

binding = "BROWSER"


[ai]

binding = "AI"


```

Explain Code

If you are using the [Cloudflare Vite plugin ↗](https://developers.cloudflare.com/workers/vite-plugin/), you need to include the following [alias ↗](https://vite.dev/config/shared-options.html#resolve-alias) in `vite.config.ts`:

TypeScript

```

export default defineConfig({

  // ...

  resolve: {

    alias: {

      playwright: "@cloudflare/playwright",

    },

  },

});


```

If you are not using the Cloudflare Vite plugin, you need to include the following [module alias ↗](https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing) to the wrangler configuration:

JSONC

```

{

  // ...

  "alias": {

    "playwright": "@cloudflare/playwright",

  },

}


```

### 3\. Write the Worker code

Copy [workersAIClient.ts ↗](https://github.com/cloudflare/playwright/blob/main/packages/playwright-cloudflare/examples/stagehand/src/worker/workersAIClient.ts) to your project.

Then, in your Worker code, import the `workersAIClient.ts` file and use it to configure a new `Stagehand` instance:

src/index.ts

```

import { Stagehand } from "@browserbasehq/stagehand";

import { z } from "zod";

import { endpointURLString } from "@cloudflare/playwright";

import { WorkersAIClient } from "./workersAIClient";


export default {

  async fetch(request: Request, env: Env) {

    if (new URL(request.url).pathname !== "/")

      return new Response("Not found", { status: 404 });


    const stagehand = new Stagehand({

      env: "LOCAL",

      localBrowserLaunchOptions: { cdpUrl: endpointURLString(env.BROWSER) },

      llmClient: new WorkersAIClient(env.AI),

      verbose: 1,

    });


    await stagehand.init();

    const page = stagehand.page;


    await page.goto("https://demo.playwright.dev/movies");


    // if search is a multi-step action, stagehand will return an array of actions it needs to act on

    const actions = await page.observe('Search for "Furiosa"');

    for (const action of actions) await page.act(action);


    await page.act("Click the search result");


    // normal playwright functions work as expected

    await page.waitForSelector(".info-wrapper .cast");


    let movieInfo = await page.extract({

      instruction: "Extract movie information",

      schema: z.object({

        title: z.string(),

        year: z.number(),

        rating: z.number(),

        genres: z.array(z.string()),

        duration: z.number().describe("Duration in minutes"),

      }),

    });


    await stagehand.close();


    return Response.json(movieInfo);

  },

};


```

Explain Code

Note

The snippet above requires [Zod v3 ↗](https://v3.zod.dev/) and is currently not compatible with Zod v4.

Ensure your `package.json` has the following dependencies:

```

{

  // ...

  "dependencies": {

    "@browserbasehq/stagehand": "2.5.x",

    "@cloudflare/playwright": "^1.0.0",

    "zod": "^3.25.76",

    "zod-to-json-schema": "^3.24.6"

    // ...

  }

}


```

Explain Code

### 4\. Build the project

Terminal window

```

npm run build


```

### 5\. Deploy to Cloudflare Workers

After you deploy, you can interact with the Worker using this URL pattern:

```

https://<your-worker>.workers.dev


```

Terminal window

```

npm run deploy


```

## Use Cloudflare AI Gateway with Workers AI

[AI Gateway](https://developers.cloudflare.com/ai-gateway/) is a service that adds observability to your AI applications. By routing your requests through AI Gateway, you can monitor and debug your AI applications.

To use AI Gateway with a third-party model, first create a gateway in the **AI Gateway** page of the Cloudflare dashboard.

[ Go to **AI Gateway** ](https://dash.cloudflare.com/?to=/:account/ai/ai-gateway) 

In this example, we've named the gateway `stagehand-example-gateway`.

TypeScript

```

const stagehand = new Stagehand({

  env: "LOCAL",

  localBrowserLaunchOptions: { cdpUrl },

  llmClient: new WorkersAIClient(env.AI, {

    gateway: {

      id: "stagehand-example-gateway",

    },

  }),

});


```

## Use a third-party model

If you want to use a model outside of Workers AI, you can configure Stagehand to use models from supported [third-party providers ↗](https://docs.stagehand.dev/configuration/models#supported-providers), including OpenAI and Anthropic, by providing your own credentials.

In this example, you will configure Stagehand to use [OpenAI ↗](https://openai.com/). You will need an OpenAI API key. Cloudflare recommends storing your API key as a [secret](https://developers.cloudflare.com/workers/configuration/secrets/).

Terminal window

```

npx wrangler secret put OPENAI_API_KEY


```

Then, configure Stagehand with your provider, model, and API key.

TypeScript

```

const stagehand = new Stagehand({

  env: "LOCAL",

  localBrowserLaunchOptions: { cdpUrl: endpointURLString(env.BROWSER) },

  modelName: "openai/gpt-4.1",

  modelClientOptions: {

    apiKey: env.OPENAI_API_KEY,

  },

});


```

## Use Cloudflare AI Gateway with a third-party model

[AI Gateway](https://developers.cloudflare.com/ai-gateway/) is a service that adds observability to your AI applications. By routing your requests through AI Gateway, you can monitor and debug your AI applications.

To use AI Gateway with a third-party model, first create a gateway in the **AI Gateway** page of the Cloudflare dashboard.

[ Go to **AI Gateway** ](https://dash.cloudflare.com/?to=/:account/ai/ai-gateway) 

In this example, we are using [OpenAI with AI Gateway](https://developers.cloudflare.com/ai-gateway/usage/providers/openai/). Make sure to add the `baseURL` as shown below, with your own Account ID and Gateway ID.

You must specify the `apiKey` in the `modelClientOptions`:

TypeScript

```

const stagehand = new Stagehand({

  env: "LOCAL",

  localBrowserLaunchOptions: { cdpUrl: endpointURLString(env.BROWSER) },

  modelName: "openai/gpt-4.1",

  modelClientOptions: {

    apiKey: env.OPENAI_API_KEY,

    baseURL: `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai`,

  },

});


```

If you are using an authenticated AI Gateway, follow the instructions in [AI Gateway authentication](https://developers.cloudflare.com/ai-gateway/configuration/authentication/) and include `cf-aig-authorization` as a header.

## Stagehand API

For the full list of Stagehand methods and capabilities, refer to the official [Stagehand API documentation ↗](https://docs.stagehand.dev/first-steps/introduction).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/stagehand/","name":"Stagehand"}}]}
```

---

---
title: FAQ
description: Below you will find answers to our most commonly asked questions about Browser Run (formerly Browser Rendering).
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/faq.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# FAQ

Below you will find answers to our most commonly asked questions about Browser Run (formerly Browser Rendering).

For pricing questions, visit the [pricing FAQ](https://developers.cloudflare.com/browser-run/pricing/#pricing-faq). For usage limits questions, visit the [limits FAQ](https://developers.cloudflare.com/browser-run/limits/#faq). If you cannot find the answer you are looking for, join us on [Discord ↗](https://discord.cloudflare.com).

---

## Errors & Troubleshooting

### Error: Cannot read properties of undefined (reading 'fetch')

This error typically occurs because your Puppeteer launch is not receiving the browser binding. To resolve this error, pass your browser binding into `puppeteer.launch`.

### Error: 429 browser time limit exceeded

This error (`Unable to create new browser: code: 429: message: Browser time limit exceeded for today`) indicates you have hit the daily browser-instance limit on the Workers Free plan. [Workers Free plan accounts are capped at 10 minutes of browser use a day](https://developers.cloudflare.com/browser-run/limits/#workers-free). Once you exceed that limit, further creation attempts return a 429 error until the next UTC day.

To resolve this error, [upgrade to a Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/) which allows for more than 10 minutes of usage a day and has higher [limits](https://developers.cloudflare.com/browser-run/limits/#workers-paid). If you recently upgraded but still see this error, try redeploying your Worker to ensure your usage is correctly associated with your new plan.

### Error: 422 unprocessable entity

A `422 Unprocessable Entity` error usually means that Browser Run was not able to complete an action because of an issue with the site.

This can happen if:

* The website consumes too much memory during rendering.
* The page itself crashed or returned an error before the action completed.
* The request exceeded one of the [timeout limits](https://developers.cloudflare.com/browser-run/reference/timeouts/) for page load, element load, or an action.

Most often, this error is caused by a timeout. You can review the different timers and their limits in the [Quick Actions timeouts reference](https://developers.cloudflare.com/browser-run/reference/timeouts/).

### Why is my page content missing or incomplete?

If your screenshots, PDFs, or scraped content are missing elements that appear when viewing the page in a browser, the page likely has not finished loading before Browser Run captures the output.

JavaScript-heavy pages and Single Page Applications (SPAs) often load content dynamically after the initial HTML is parsed. By default, Browser Run waits for `domcontentloaded`, which fires before JavaScript has finished rendering the page.

To fix this, use the `goToOptions.waitUntil` parameter with one of these values:

| Value        | Use when                                                                                                         |
| ------------ | ---------------------------------------------------------------------------------------------------------------- |
| networkidle0 | The page must be completely idle (no network requests for 500 ms). Best for pages that load all content upfront. |
| networkidle2 | The page can have up to 2 ongoing connections (like analytics or websockets). Best for most dynamic pages.       |

Quick Actions example:

```

{

  "url": "https://example.com",

  "goToOptions": {

    "waitUntil": "networkidle2"

  }

}


```

If content is still missing:

* Use `waitForSelector` to wait for a specific element to appear before capturing.
* Increase `goToOptions.timeout` (up to 60 seconds) for slow-loading pages.
* Check if the page requires authentication or returns different content to bots.

For a complete reference, see [Quick Actions timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/).

---

## Getting started & Development

### Does local development support all Browser Run features?

Not yet. Local development currently has the following limitation(s):

* Requests larger than 1 MB are not supported.

You can also run Chrome in visible (headful) mode during local development to visually debug your automation scripts (experimental). Set the `X_BROWSER_HEADFUL` environment variable before starting your dev server:

Terminal window

```

X_BROWSER_HEADFUL=true npx wrangler dev


```

Use real headless browser during local development

To interact with a real headless browser during local development, set `"remote" : true` in the Browser binding configuration. Learn more in our [remote bindings documentation](https://developers.cloudflare.com/workers/development-testing/#remote-bindings).

### How do I render authenticated pages using Quick Actions?

If the page you are rendering requires authentication, you can pass credentials using one of the following methods. These parameters work with all [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) endpoints.

HTTP Basic Auth:

```

{

  "authenticate": {

    "username": "user",

    "password": "pass"

  }

}


```

Cookie-based authentication:

```

{

  "cookies": [

    {

      "name": "session_id",

      "value": "abc123",

      "domain": "example.com",

      "path": "/",

      "secure": true,

      "httpOnly": true

    }

  ]

}


```

Explain Code

Token-based authentication:

```

{

  "setExtraHTTPHeaders": {

    "Authorization": "Bearer your-token"

  }

}


```

For complete working examples of all three methods, refer to [Capture a screenshot of an authenticated page](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/#capture-a-screenshot-of-an-authenticated-page).

### Will Browser Run be detected by Bot Management?

Yes, Browser Run requests are always identified as bot traffic by Cloudflare. Cloudflare does not enforce bot protection by default — that is the customer's choice.

If you are attempting to scan your own zone and want Browser Run to access your website freely without your bot protection configuration interfering, you can create a WAF skip rule to [allowlist Browser Run](https://developers.cloudflare.com/browser-run/faq/#can-i-allowlist-browser-run-on-my-own-website).

### Can I allowlist Browser Run on my own website?

You must be on an Enterprise plan to allowlist Browser Run on your own website because WAF custom rules require access to [Bot Management](https://developers.cloudflare.com/bots/get-started/bot-management/) fields.

Browser Run uses different [bot detection IDs](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#bot-detection) depending on the method. Use the ID that matches the method you want to allowlist.

1. In the Cloudflare dashboard, go to the **Security rules** page of your account and domain.  
[ Go to **Security rules** ](https://dash.cloudflare.com/?to=/:account/:zone/security/security-rules)
2. To create a new empty rule, select **Create rule** \> **Custom rules**.
3. Enter a descriptive name for the rule in **Rule name**, such as `Allow Browser Run`.
4. Under **When incoming requests match**, use the **Field** dropdown to choose _Bot Detection ID_. For **Operator**, select _equals_. For **Value**, enter the [bot detection ID](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#bot-detection) for the method you want to allowlist.
5. Under **Then take action**, in the **Choose action** dropdown, select **Skip**.
6. Under **Place at**, select the order of the rule in the **Select order** dropdown to be **First**. Setting the order as **First** allows this rule to be applied before subsequent rules.
7. To save and deploy your rule, select **Deploy**.

### Does Browser Run rotate IP addresses for outbound requests?

No. Browser Run requests originate from Cloudflare's global network and you cannot configure per-request IP rotation. All rendering traffic comes from Cloudflare IP ranges and requests include [automatic headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/), such as `cf-biso-request-id` and `cf-biso-devtools` so origin servers can identify them.

### Is there a limit to how many requests a single browser session can handle?

There is no fixed limit on the number of requests per browser session. A single browser can handle multiple requests as long as it stays within available compute and memory limits.

### Can I use custom fonts in Browser Run?

Yes. If your webpage or PDF requires a font that is not pre-installed, you can load custom fonts at render time using `addStyleTag`. This works with [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/), [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), and [Playwright](https://developers.cloudflare.com/browser-run/playwright/). For instructions and examples, refer to [Custom fonts](https://developers.cloudflare.com/browser-run/features/custom-fonts/).

### How can I manage concurrency and session isolation with Browser Run?

If you are hitting concurrency [limits](https://developers.cloudflare.com/browser-run/limits/#workers-paid), or want to optimize concurrent browser usage, here are a few tips:

* Optimize with tabs or shared browsers: Instead of launching a new browser for each task, consider opening multiple tabs or running multiple actions within the same browser instance.
* [Reuse sessions](https://developers.cloudflare.com/browser-run/features/reuse-sessions/): You can optimize your setup and decrease startup time by reusing sessions instead of launching a new browser every time. If you are concerned about maintaining test isolation (for example, for tests that depend on a clean environment), we recommend using [incognito browser contexts ↗](https://pptr.dev/api/puppeteer.browser.createbrowsercontext), which isolate cookies and cache with other sessions.

If you are still running into concurrency limits you can [request a higher limit ↗](https://forms.gle/CdueDKvb26mTaepa9).

---

## Security & Data Handling

### Does Cloudflare store or retain the HTML content I submit for rendering?

No. Cloudflare processes content ephemerally and does not retain customer-submitted HTML or generated output (such as PDFs or screenshots) beyond what is required to perform the rendering operation. Once the response is returned, the content is immediately discarded from the rendering environment.

This applies to all integration methods, including [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/), [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), and [CDP](https://developers.cloudflare.com/browser-run/cdp/).

### Is there any temporary caching of submitted content?

For [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/), generated content is cached by default for five seconds (configurable up to one day via the `cacheTTL` parameter, or set to `0` to disable caching). This cache protects against repeated requests for the same URL by the same account. Customer-submitted HTML content itself is not cached.

For [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), and [CDP](https://developers.cloudflare.com/browser-run/cdp/), no caching is used. Content exists only in memory for the duration of the rendering operation and is discarded immediately after the response is returned.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/faq/","name":"FAQ"}}]}
```

---

---
title: Limits
description: Learn about the limits associated with Browser Run.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/limits.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Limits

Browser Run limits are based on your [Cloudflare Workers plan](https://developers.cloudflare.com/workers/platform/pricing/).

For pricing information, refer to [Browser Run pricing](https://developers.cloudflare.com/browser-run/pricing/).

## Workers Free

Need higher limits?

If you are on a Workers Free plan and you want to increase your limits, upgrade to a Workers Paid plan in the **Workers plans** page of the Cloudflare dashboard:

[ Go to **Workers plans** ](https://dash.cloudflare.com/?to=/:account/workers/plans)

| Feature                                                                         | Limit                              |
| ------------------------------------------------------------------------------- | ---------------------------------- |
| Browser hours                                                                   | 10 minutes per day                 |
| Concurrent browsers per account (Browser Sessions only) [1](#user-content-fn-1) | 3 per account                      |
| New browser instances (Browser Sessions only)                                   | 1 every 20 seconds                 |
| Browser timeout                                                                 | 60 seconds [2](#user-content-fn-2) |
| Total requests (Quick Actions only) [3](#user-content-fn-3)                     | 1 every 10 seconds                 |

### `/crawl` endpoint limits

The [/crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/) has additional limits for Workers Free plan users:

| Feature                 | Limit     |
| ----------------------- | --------- |
| Crawl jobs per day      | 5 per day |
| Maximum pages per crawl | 100 pages |

## Workers Paid

Need higher limits?

If you are on a Workers Paid plan and you want to increase your limits beyond those listed here, Cloudflare will grant [requests for higher limits ↗](https://forms.gle/CdueDKvb26mTaepa9) on a case-by-case basis.

| Feature                                                                         | Limit                                                                                   |
| ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Browser hours                                                                   | No limit ([See pricing](https://developers.cloudflare.com/browser-run/pricing/))        |
| Concurrent browsers per account (Browser Sessions only) [1](#user-content-fn-1) | 120 per account ([See pricing](https://developers.cloudflare.com/browser-run/pricing/)) |
| New browser instances per second (Browser Sessions only)                        | 1 per second                                                                            |
| Browser timeout                                                                 | 60 seconds [2](#user-content-fn-2)                                                      |
| Total requests per second (Quick Actions only) [3](#user-content-fn-3)          | 10 per second                                                                           |

## FAQ

### How can I manage concurrency and session isolation with Browser Run?

If you are hitting concurrency [limits](https://developers.cloudflare.com/browser-run/limits/#workers-paid), or want to optimize concurrent browser usage, here are a few tips:

* Optimize with tabs or shared browsers: Instead of launching a new browser for each task, consider opening multiple tabs or running multiple actions within the same browser instance.
* [Reuse sessions](https://developers.cloudflare.com/browser-run/features/reuse-sessions/): You can optimize your setup and decrease startup time by reusing sessions instead of launching a new browser every time. If you are concerned about maintaining test isolation (for example, for tests that depend on a clean environment), we recommend using [incognito browser contexts ↗](https://pptr.dev/api/puppeteer.browser.createbrowsercontext), which isolate cookies and cache with other sessions.

If you are still running into concurrency limits you can [request a higher limit ↗](https://forms.gle/CdueDKvb26mTaepa9).

### Can I increase the browser timeout?

By default, a browser instance will time out after 60 seconds of inactivity. If you want to keep the browser open longer, you can use the [keep\_alive option](https://developers.cloudflare.com/browser-run/puppeteer/#keep-alive), which allows you to extend the timeout to up to 10 minutes.

### Is there a maximum session duration?

There is no fixed maximum lifetime for a browser session as long as it remains active. By default, Browser Run closes sessions after one minute of inactivity to prevent unintended usage. You can [increase this inactivity timeout](https://developers.cloudflare.com/browser-run/puppeteer/#keep-alive) to up to 10 minutes.

If you need sessions to remain open longer, keep them active by sending a command at least once within your configured inactivity window (for example, every 10 minutes). Sessions also close when Browser Run rolls out a new release.

### I upgraded from the Workers Free plan, but I'm still hitting the 10-minute per day limit. What should I do?

If you recently upgraded to the [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/) but still encounter the 10-minute per day limit, redeploy your Worker to ensure your usage is correctly associated with the new plan.

### Why is my browser usage higher than expected?

If you are hitting the daily limit or seeing higher usage than expected, the most common cause is browser sessions that are not being closed properly. When a browser session is not explicitly closed with `browser.close()`, it remains open and continues to consume browser time until it times out (60 seconds by default, or up to 10 minutes if you use the `keep_alive` option).

To minimize usage:

* Always call `browser.close()` when you are finished with a browser session.
* Wrap your browser code in a `try/finally` block to ensure `browser.close()` is called even if an error occurs.
* Use [puppeteer.history()](https://developers.cloudflare.com/browser-run/puppeteer/#list-recent-sessions) or [playwright.history()](https://developers.cloudflare.com/browser-run/playwright/#list-recent-sessions) to review recent sessions and identify any that closed due to `BrowserIdle` instead of `NormalClosure`. Sessions that close due to idle timeout indicate the browser was not closed explicitly.

You can monitor your usage and view session close reasons in the Cloudflare dashboard on the **Browser Run** page:

[ Go to **Browser Run** ](https://dash.cloudflare.com/?to=/:account/workers/browser-run) 

Refer to [Browser close reasons](https://developers.cloudflare.com/browser-run/reference/browser-close-reasons/) for more information.

## Troubleshooting

### Error: `429 Too many requests`

When you make too many requests in a short period of time, Browser Run will respond with HTTP status code `429 Too many requests`. You can view your account's rate limits in the [Workers Free](#workers-free) and [Workers Paid](#workers-paid) sections above.

The example below demonstrates how to handle rate limiting gracefully by reading the `Retry-After` value and retrying the request after that delay.

* [ Quick Actions ](#tab-panel-3570)
* [ Puppeteer ](#tab-panel-3571)

JavaScript

```

const response = await fetch('https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/content', {

    method: 'POST',

    headers: {

        'Content-Type': 'application/json',

        'Authorization': 'Bearer <your-token>',

    },

    body: JSON.stringify({ url: 'https://example.com' })

});


if (response.status === 429) {

const retryAfter = response.headers.get('Retry-After');

console.log(`Rate limited. Waiting ${retryAfter} seconds...`);

await new Promise(resolve => setTimeout(resolve, retryAfter \* 1000));


    // Retry the request

    const retryResponse = await fetch(/* same request as above */);


}


```

Explain Code

JavaScript

```

import puppeteer from "@cloudflare/puppeteer";


try {

  const browser = await puppeteer.launch(env.MYBROWSER);


  const page = await browser.newPage();

  await page.goto("https://example.com");

  const content = await page.content();


  await browser.close();

} catch (error) {

  if (error.status === 429) {

    const retryAfter = error.headers.get("Retry-After");

    console.log(

      `Browser instance limit reached. Waiting ${retryAfter} seconds...`,

    );

    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));


    // Retry launching browser

    const browser = await puppeteer.launch(env.MYBROWSER);

  }

}


```

Explain Code

### Error: `429 Browser time limit exceeded for today`

This `Error processing the request: Unable to create new browser: code: 429: message: Browser time limit exceeded for today` error indicates you have hit the daily browser limit on the Workers Free plan. [Workers Free plan accounts are limited](#workers-free) to 10 minutes of Browser Run usage per day. If you exceed that limit, you will receive a `429` error until the next UTC day.

You can [increase your limits](#workers-paid) by upgrading to a Workers Paid plan on the **Workers plans** page of the Cloudflare dashboard:

[ Go to **Workers plans** ](https://dash.cloudflare.com/?to=/:account/workers/plans) 

If you recently upgraded but still encounter the 10-minute per day limit, redeploy your Worker to ensure your usage is correctly associated with the new plan.

## Footnotes

1. Browsers close upon task completion or sixty seconds of inactivity (if you do not [extend your browser timeout](#can-i-increase-the-browser-timeout)). Therefore, in practice, many workflows do not require a high number of concurrent browsers. [↩](#user-content-fnref-1) [↩2](#user-content-fnref-1-2)
2. By default, a browser will time out after 60 seconds of inactivity. You can extend this to up to 10 minutes using the [keep\_alive option](https://developers.cloudflare.com/browser-run/puppeteer/#keep-alive). Call `browser.close()` to release the browser instance immediately. [↩](#user-content-fnref-2) [↩2](#user-content-fnref-2-2)
3. If you exceed the per-second rate limit, you will receive a `429` response. Refer to [troubleshooting the 429 Too many requests error](#error-429-too-many-requests). [↩](#user-content-fnref-3) [↩2](#user-content-fnref-3-2)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/limits/","name":"Limits"}}]}
```

---

---
title: Pricing
description: Billing depends on how you use Browser Run:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/pricing.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Pricing

 Available on Free and Paid plans 

Billing depends on how you use Browser Run:

* [**Quick Actions**](https://developers.cloudflare.com/browser-run/quick-actions/): Charged for browser hours only.
* **Browser Sessions** ([Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), [CDP](https://developers.cloudflare.com/browser-run/cdp/)): Direct browser control, charged for both browser hours and concurrent browsers.

Browser hours are shared across all methods.

| Workers Free                                | Workers Paid       |                                                                                                                           |
| ------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| Browser hours                               | 10 minutes per day | 10 hours per month, then $0.09 per additional hour                                                                        |
| Concurrent browsers (Browser Sessions only) | 3 browsers         | 10 browsers ([averaged monthly](#how-is-the-number-of-concurrent-browsers-calculated)), then $2.00 per additional browser |

## Examples of Workers Paid pricing

#### Example: Quick Actions pricing

If a Workers Paid user uses Quick Actions for 50 hours during the month, the estimated cost for the month is as follows.

For browser hours:

  
50 hours - 10 hours (included in plan) = 40 hours

  
40 hours × $0.09 per hour = $3.60

#### Example: Browser Sessions pricing

If a Workers Paid plan user uses Browser Sessions (Puppeteer, Playwright, or CDP) for 50 hours during the month, and uses 10 concurrent browsers for the first 15 days and 20 concurrent browsers the last 15 days, the estimated cost for the month is as follows.

For browser hours:

  
50 hours - 10 hours (included in plan) = 40 hours

  
40 hours × $0.09 per hour = $3.60

For concurrent browsers:

  
((10 browsers × 15 days) + (20 browsers × 15 days)) = 450 total browsers used in month

  
450 browsers used in month ÷ 30 days in month = 15 browsers (averaged monthly)

  
15 browsers (averaged monthly) − 10 (included in plan) = 5 browsers  
5 browsers × $2.00 per browser = $10.00

For browser hours and concurrent browsers:

  
$3.60 + $10.00 = $13.60

## Pricing FAQ

### How do I estimate my Browser Run costs?

You can monitor Browser Run usage in two ways:

* To monitor your Browser Run usage in the Cloudflare dashboard, go to the **Browser Run** page.  
[ Go to **Browser Run** ](https://dash.cloudflare.com/?to=/:account/workers/browser-run)
* The `X-Browser-Ms-Used` header, which is returned in every Quick Actions response, reports browser time used for the request (in milliseconds). You can also access this header using the Typescript SDK with the .asResponse() method:  
TypeScript  
```  
const contentRes = await client.browserRendering.content  
  .create({  
    account_id: "account_id",  
  })  
  .asResponse();  
const browserMsUsed = parseInt(  
  contentRes.headers.get("X-Browser-Ms-Used") || "",  
);  
```

You can then use the tables above to estimate your costs based on your usage.

### Do failed API calls, such as those that time out, add to billable browser hours?

No. If a Quick Actions request fails with a `waitForTimeout` error, the browser session is not charged.

### How is the number of concurrent browsers calculated?

Cloudflare calculates concurrent browsers as the monthly average of your daily peak usage. In other words, we record the peak number of concurrent browsers each day and then average those values over the month. This approach reflects your typical traffic and ensures you are not disproportionately charged for brief spikes in browser concurrency.

### How is billing time calculated?

At the end of each day, Cloudflare totals all of your browser usage for that day in seconds. At the end of each billing cycle, we add up all of the daily totals to find the monthly total of browser hours, rounded to the nearest whole hour. In other words, 1,800 seconds (30 minutes) or more is rounded up to the nearest hour, and 1,799 seconds or less is rounded down to the nearest whole hour.

For example, if you only use one minute of browser time in a day, that day counts as one minute. If you do that every day for a 30-day month, your total would be 30 minutes. For billing, we round that up to one browser hour.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/pricing/","name":"Pricing"}}]}
```

---

---
title: Changelog
description: Review recent changes to Browser Run.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/changelog.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Changelog

This is a detailed changelog of every update to Browser Run (formerly Browser Rendering). For a higher-level summary of major updates to every Cloudflare product, including Browser Run, visit [developers.cloudflare.com/changelog](https://developers.cloudflare.com/changelog/).

[ Subscribe to RSS ](https://developers.cloudflare.com/browser-run/changelog/index.xml)

## 2026-04-15

**@cloudflare/playwright v1.3.0 released**
* Released version 1.3.0 of [@cloudflare/playwright](https://github.com/cloudflare/playwright/releases/tag/v1.3.0). Starting with this version, the library uses the standard CDP (Chrome DevTools Protocol) internally to communicate with Browser Run, replacing the previous chunked protocol. This aligns with Browser Run's [full CDP support](https://developers.cloudflare.com/changelog/post/2026-04-10-browser-rendering-cdp-endpoint/) and prevents compatibility issues when using the latest compatibility dates. If you encounter any issues, you can downgrade by setting a `compatibility_date` prior to `2026-03-17` or by adding the `no_websocket_standard_binary_type` flag. Refer to the [@cloudflare/playwright README](https://github.com/cloudflare/playwright?tab=readme-ov-file#cdp-protocol-support) for details.

## 2026-04-15

**Higher concurrency limits**
* Increased the default [concurrent browser limit](https://developers.cloudflare.com/browser-run/limits/#workers-paid) for Workers Paid plans from 30 to **120 per account**.
* Increased new browser instance rate for Workers Paid plans from 30 per minute to **1 per second**.
* Rate limits across the [limits page](https://developers.cloudflare.com/browser-run/limits/) are now expressed in per-second terms, matching how they are enforced.

## 2026-04-15

**Live View**
* [Live View](https://developers.cloudflare.com/browser-run/features/live-view/) lets you see and interact with a remote browser session in real time. Use it to debug automation scripts, monitor what a browser is doing, or manually step in when a task requires human intervention. Access Live View from the Cloudflare dashboard, via the hosted UI at `live.browser.run`, or using native Chrome DevTools.

## 2026-04-15

**Human in the Loop**
* [Human in the Loop](https://developers.cloudflare.com/browser-run/features/human-in-the-loop/) lets a human step into a live browser session to handle what automation cannot, such as login pages, CAPTCHAs, or sensitive data entry, then hand control back to the script. Access any active session through [Live View](https://developers.cloudflare.com/browser-run/features/live-view/).

## 2026-04-15

**Session Recordings**
* [Session Recordings](https://developers.cloudflare.com/browser-run/features/session-recording/) captures DOM changes, mouse and keyboard events, and page navigation as structured data so you can replay any browser session after it ends. Enable recordings by passing `recording: true` when launching a browser. After the session closes, access recordings from the **Runs** tab in the Cloudflare dashboard or retrieve them via API.

## 2026-04-15

**WebMCP support**
* Browser Run now supports [WebMCP](https://developers.cloudflare.com/browser-run/features/webmcp/) (Web Model Context Protocol), which allows websites to declare structured tools that AI agents can discover and execute. WebMCP-enabled browsers are available through the experimental lab browser pool.

## 2026-04-14

**Wrangler CLI commands for Browser Rendering**
* Added `wrangler browser` commands to create, manage, and view browser sessions directly from the terminal. Available commands: `create`, `close`, `list`, and `view`. For full usage details, refer to [Wrangler commands](https://developers.cloudflare.com/browser-run/reference/wrangler-commands/).

## 2026-04-13

**@cloudflare/puppeteer v1.1.0 released**
* Released version 1.1.0 of [@cloudflare/puppeteer](https://github.com/cloudflare/puppeteer/releases/tag/v1.1.0), which replaces the internal chunked protocol with plain CDP. This fixes a compatibility issue when using the latest compatibility dates.

## 2026-04-10

**Chrome DevTools Protocol (CDP) and MCP client support**
* Browser Rendering now exposes the [Chrome DevTools Protocol (CDP)](https://developers.cloudflare.com/browser-run/cdp/) as an endpoint. Any CDP-compatible client, including [Puppeteer](https://developers.cloudflare.com/browser-run/cdp/puppeteer/) and [Playwright](https://developers.cloudflare.com/browser-run/cdp/playwright/), can connect from any environment, whether that is [Cloudflare Workers](https://developers.cloudflare.com/workers/), your local machine, or a cloud environment. [MCP clients](https://developers.cloudflare.com/browser-run/cdp/mcp-clients/) like Claude Desktop, Claude Code, Cursor, and OpenCode can also use Browser Rendering as their remote browser.

## 2026-04-06

**Local development: headful mode (experimental)**
* You can now run Chrome in visible (headful) mode during local development by setting `X_BROWSER_HEADFUL=true` before running `wrangler dev` or `vite dev`. This makes it easier to visually debug your browser automation scripts. This feature is experimental and may change without notice.

## 2026-03-23

**@cloudflare/playwright v1.2.0 released**
* Released version 1.2.0 of [@cloudflare/playwright](https://github.com/cloudflare/playwright/releases/tag/v1.2.0), now upgraded to [Playwright v1.58.2](https://playwright.dev/docs/release-notes#version-158).

## 2026-03-17

**Separate bot detection IDs for Browser Rendering methods**
* Browser Rendering now uses separate bot detection IDs for the [REST API](https://developers.cloudflare.com/browser-run/quick-actions/) and [Browser Sessions](https://developers.cloudflare.com/browser-run/#integration-methods) versus the [crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/), allowing you to identify and control each method independently. For the full list of IDs, refer to [Automatic request headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#bot-detection).

## 2026-03-10

**New REST API endpoint: /crawl (Beta)**
* Added the [/crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/) (beta) to the REST API. The `/crawl` endpoint scrapes content from a starting URL and follows links across the site, up to a configurable depth or page limit. Responses can be returned as HTML, Markdown, or structured JSON (powered by [Workers AI](https://developers.cloudflare.com/workers-ai/)).

## 2026-03-04

**Increased REST API rate limits**
* Increased [REST API rate limits](https://developers.cloudflare.com/browser-run/limits/#workers-paid) for Workers Paid plans from 180 requests per minute (3 per second) to 600 requests per minute (10 per second). No action is needed to benefit from the higher limits.

## 2026-02-26

**New tutorial: Generate OG images for Astro sites**
* Added a new tutorial on how to [generate OG images for Astro sites](https://developers.cloudflare.com/browser-run/how-to/og-images-astro/) using Browser Rendering. The tutorial walks through creating an Astro template, using Browser Rendering to screenshot it as a PNG, and serving the generated images.

## 2026-02-24

**Documentation updates for robots.txt and sitemaps**
* Added [robots.txt and sitemaps reference page](https://developers.cloudflare.com/browser-run/reference/robots-txt/) with guidance on configuring robots.txt and sitemaps for sites accessed by Browser Rendering, including sitemap index files and caching headers.

## 2026-02-18

**@cloudflare/playwright v1.1.1 released**
* Released version 1.1.1 of [@cloudflare/playwright](https://github.com/cloudflare/playwright/releases/tag/v1.1.1), which includes a bug fix that resolves a chunking issue that could occur when generating large PDFs. Upgrade to this version to avoid this issue.

## 2026-02-03

**@cloudflare/puppeteer v1.0.6 released**
* Released version 1.0.6 of [@cloudflare/puppeteer](https://github.com/cloudflare/puppeteer/releases/tag/v1.0.6), which includes a fix for rendering large text PDFs.

## 2026-01-21

**@cloudflare/puppeteer v1.0.5 released**
* Released version 1.0.5 of [@cloudflare/puppeteer](https://www.npmjs.com/package/@cloudflare/puppeteer/v/1.0.5), which includes a performance optimization for base64 decoding.

## 2026-01-08

**@cloudflare/playwright v1.1.0 released**
* Released version 1.1.0 of [@cloudflare/playwright](https://github.com/cloudflare/playwright), now upgraded to [Playwright v1.57.0](https://playwright.dev/docs/release-notes#version-157).

## 2026-01-07

**Bug fixes for JSON endpoint, waitForSelector timeout, and WebSocket rendering**
* Updated the [/json endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/) fallback model and improved error handling for when plan limits of Workers Free plan users are reached.
* REST API requests using `waitForSelector` will now correctly fail if the specified selector is not found within the time limit.
* Fixed an issue where pages using WebSockets were not rendering correctly.

## 2025-12-04

**Added guidance on allowlisting Browser Rendering in Bot Management**
* Added [FAQ guidance](https://developers.cloudflare.com/browser-run/faq/#can-i-allowlist-browser-run-on-my-own-website) on how to create a WAF skip rule to allowlist Browser Rendering requests when using Bot Management on your zone.

## 2025-12-03

**Improved AI JSON response parsing and debugging**
* Added `rawAiResponse` field to [/json endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/) error responses, allowing you to inspect the unparsed AI output when JSON parsing fails for easier debugging.
* Improved AI response handling to better distinguish between valid JSON objects, arrays, and invalid payloads, increasing type safety and reliability.

## 2025-10-21

**Added guidance on REST API timeouts and custom fonts**
* Added [REST API timeouts](https://developers.cloudflare.com/browser-run/reference/timeouts/) page explaining how Browser Rendering uses independent timers (for page load, selectors, and actions) and how to configure them.
* Updated [Supported fonts](https://developers.cloudflare.com/browser-run/reference/supported-fonts/) guide with instructions on using your own custom fonts via `addStyleTag()` in [Playwright](https://developers.cloudflare.com/browser-run/playwright/) or [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/).

## 2025-09-25

**Updates to Playwright, new support for Stagehand, and increased limits**
* [Playwright](https://developers.cloudflare.com/browser-run/playwright/) support in Browser Rendering is now GA. We've upgraded to [Playwright v1.55](https://playwright.dev/docs/release-notes#version-155).
* Added support for [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/), an open source browser automation framework, powered by [Workers AI](https://developers.cloudflare.com/workers-ai). Stagehand enables developers to build more reliably and flexibly by combining code with natural-language instructions.
* Increased [limits](https://developers.cloudflare.com/browser-run/limits/#workers-paid) for paid plans on both the [REST API](https://developers.cloudflare.com/browser-run/quick-actions/) and [Browser Sessions](https://developers.cloudflare.com/browser-run/#integration-methods).

## 2025-09-22

**Added \`excludeExternalLinks\` parameter to \`/links\` REST endpoint**
* Added `excludeExternalLinks` parameter when using the [/links endpoint](https://developers.cloudflare.com/browser-run/quick-actions/links-endpoint/). When set to `true`, links pointing to outside the domain of the requested URL are excluded.

## 2025-09-02

**Added \`X-Browser-Ms-Used\` response header**
* Each REST API response now includes the `X-Browser-Ms-Used` response header, which reports the browser time (in milliseconds) used by the request.

## 2025-08-20

**Browser Rendering billing goes live**
* Billing for Browser Rendering begins today, August 20th, 2025\. See [pricing page](https://developers.cloudflare.com/browser-run/pricing/) for full details. You can monitor usage via the [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/workers/browser-run).

## 2025-08-18

**Wrangler updates to local dev**
* Improved the local development experience by updating the method for downloading the dev mode browser and added support for [/v1/sessions endpoint](https://developers.cloudflare.com/platform/puppeteer/#list-open-sessions), allowing you to list open browser rendering sessions. Upgrade to `wrangler@4.31.0` to get started.

## 2025-07-29

**Updates to Playwright, local dev support, and REST API**
* [Playwright](https://developers.cloudflare.com/browser-run/playwright/) upgraded to [Playwright v1.54.1](https://github.com/microsoft/playwright/releases/tag/v1.54.1) and [Playwright MCP](https://developers.cloudflare.com/browser-run/playwright/playwright-mcp/) upgraded to be in sync with upstream Playwright MCP v0.0.30.
* Local development with `npx wrangler dev` now supports [Playwright](https://developers.cloudflare.com/browser-run/playwright/) when using Browser Rendering. Upgrade to the latest version of wrangler to get started.
* The [/content endpoint](https://developers.cloudflare.com/browser-run/quick-actions/content-endpoint/) now returns the page's title, making it easier to identify pages.
* The [/json endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/) now allows you to specify your own AI model for the extraction, using the `custom_ai` parameter.
* The default viewport size on the [/screenshot endpoint](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/) has been increased from 800x600 to 1920x1080\. You can still override the viewport via request options.

## 2025-07-25

**@cloudflare/puppeteer 1.0.4 released**
* We have released version 1.0.4 of [@cloudflare/puppeteer](https://github.com/cloudflare/puppeteer), now in sync with Puppeteer v22.13.1.

## 2025-07-24

**Playwright now supported in local development**
* You can now use Playwright with local development. Upgrade to [wrangler@4.26.0](mailto:wrangler@4.26.0) to get started.

## 2025-07-16

**Pricing update to Browser Rendering**
* Billing for Browser Rendering starts on August 20, 2025, with usage beyond the included [limits](https://developers.cloudflare.com/browser-run/limits/) charged according to the new [pricing rates](https://developers.cloudflare.com/browser-run/pricing/).

## 2025-07-03

**Local development support**
* We added local development support to Browser Rendering, making it simpler than ever to test and iterate before deploying.

## 2025-06-30

**New Web Bot Auth headers**
* Browser Rendering now supports [Web Bot Auth](https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/) by automatically attaching `Signature-agent`, `Signature`, and `Signature-input ` headers to verify that a request originates from Cloudflare Browser Rendering.

## 2025-06-27

**Bug fix to debug log noise in Workers**
* Fixed an issue where all debug logging was on by default and would flood logs. Debug logs is now off by default but can be re-enabled by setting [process.env.DEBUG](https://pptr.dev/guides/debugging#log-devtools-protocol-traffic) when needed.

## 2025-05-26

**Playwright MCP**
* You can now deploy [Playwright MCP](https://developers.cloudflare.com/browser-run/playwright/playwright-mcp/) and use any MCP client to get AI models to interact with Browser Rendering.

## 2025-04-30

**Automatic Request Headers**
* [Clarified Automatic Request headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/) in Browser Rendering. These headers are unique to Browser Rendering, and are automatically included and cannot be removed or overridden.

## 2025-04-07

**New free tier and REST API GA with additional endpoints**
* Browser Rendering now has a new free tier.
* The [REST API](https://developers.cloudflare.com/browser-run/quick-actions/) is Generally Available.
* Released new endpoints [/json](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/), [/links](https://developers.cloudflare.com/browser-run/quick-actions/links-endpoint/), and [/markdown](https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/).

## 2025-04-04

**Playwright support**
* You can now use [Playwright's](https://developers.cloudflare.com/browser-run/playwright/) browser automation capabilities from Cloudflare Workers.

## 2025-02-27

**New Browser Rendering REST API**
* Released a new [REST API](https://developers.cloudflare.com/browser-run/quick-actions/) in open beta. Available to all customers with a Workers Paid Plan.

## 2025-01-31

**Increased limits**
* Increased the limits on the number of concurrent browsers, and browsers per minute from 2 to 10.

## 2024-08-08

**Update puppeteer to 21.1.0**
* Rebased the fork on the original implementation up till version 21.1.0

## 2024-04-02

**Browser Rendering Available for everyone**
* Browser Rendering is now out of beta and available to all customers with Workers Paid Plan. Analytics and logs are available in Cloudflare's dashboard, under "Worker & Pages".

## 2023-05-19

**Browser Rendering Beta**
* Beta Launch

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/changelog/","name":"Changelog"}}]}
```

---

---
title: MCP server
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/mcp-server.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# MCP server

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/mcp-server/","name":"MCP server"}}]}
```

---

---
title: Custom fonts
description: Learn how to add custom fonts to Browser Run for use in screenshots and PDFs.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/features/custom-fonts.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Custom fonts

Browser Run uses a managed Chromium environment that includes a [standard set of pre-installed fonts](https://developers.cloudflare.com/browser-run/reference/supported-fonts/). When you generate a screenshot or PDF, text is rendered using the fonts available in this environment. If your page specifies a font that is not pre-installed, Chromium will automatically fall back to a similar supported font.

If you need a specific font that is not pre-installed, you can inject it into the page at render time. You can load fonts from an external URL or embed them directly as a Base64 string.

How you add a custom font depends on how you are using Browser Run:

* If you are using [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/), refer to the [Browser sessions](#browser-sessions) section.
* If you are using [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/), refer to the [Quick Actions](#quick-actions) section.

## Browser sessions

Use `addStyleTag` to inject a `@font-face` rule into the page before capturing your screenshot or PDF. You can load the font file from a CDN URL or embed it as a Base64-encoded string.

The examples below use [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) with [Workers Bindings](https://developers.cloudflare.com/browser-run/puppeteer/#use-puppeteer-in-a-worker). If you are connecting via [CDP](https://developers.cloudflare.com/browser-run/cdp/), the only difference is how you connect to the browser. Once connected, `page.addStyleTag()` works the same way. Refer to [CDP connection example](#cdp-connection-example) for details.

### From a CDN URL

* [  JavaScript ](#tab-panel-3542)
* [  TypeScript ](#tab-panel-3543)

Example with [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) and a CDN source:

JavaScript

```

const browser = await puppeteer.launch(env.MYBROWSER);

const page = await browser.newPage();

await page.addStyleTag({

  content: `

    @font-face {

      font-family: 'CustomFont';

      src: url('https://your-cdn.com/fonts/MyFont.woff2') format('woff2');

      font-weight: normal;

      font-style: normal;

    }


    body {

      font-family: 'CustomFont', sans-serif;

    }

  `,

});


```

Explain Code

Example with [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) and a CDN source:

TypeScript

```

const browser = await puppeteer.launch(env.MYBROWSER);

const page = await browser.newPage();

await page.addStyleTag({

  content: `

    @font-face {

      font-family: 'CustomFont';

      src: url('https://your-cdn.com/fonts/MyFont.woff2') format('woff2');

      font-weight: normal;

      font-style: normal;

    }


    body {

      font-family: 'CustomFont', sans-serif;

    }

  `,

});


```

Explain Code

### Base64-encoded

The following examples use [Playwright](https://developers.cloudflare.com/browser-run/playwright/), but this method works the same way with [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/).

* [  JavaScript ](#tab-panel-3544)
* [  TypeScript ](#tab-panel-3545)

Example with a Base64-encoded data source:

JavaScript

```

const browser = await playwright.launch(env.MYBROWSER);

const page = await browser.newPage();

await page.addStyleTag({

  content: `

    @font-face {

      font-family: 'CustomFont';

      src: url('data:font/woff2;base64,<BASE64_STRING>') format('woff2');

      font-weight: normal;

      font-style: normal;

    }


    body {

      font-family: 'CustomFont', sans-serif;

    }

  `,

});


```

Explain Code

Example with a Base64-encoded data source:

TypeScript

```

const browser = await playwright.launch(env.MYBROWSER);

const page = await browser.newPage();

await page.addStyleTag({

  content: `

    @font-face {

      font-family: 'CustomFont';

      src: url('data:font/woff2;base64,<BASE64_STRING>') format('woff2');

      font-weight: normal;

      font-style: normal;

    }


    body {

      font-family: 'CustomFont', sans-serif;

    }

  `,

});


```

Explain Code

### CDP connection example

When connecting via [CDP](https://developers.cloudflare.com/browser-run/cdp/), you connect to the browser using a WebSocket endpoint instead of a Workers Binding. Once connected, you use `page.addStyleTag()` the same way as the examples above.

JavaScript

```

import puppeteer from "puppeteer-core";


const ACCOUNT_ID = "your-account-id";

const API_TOKEN = "your-api-token";


// Create a browser session via CDP

const response = await fetch(

  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-rendering/devtools/browser`,

  {

    method: "POST",

    headers: { Authorization: `Bearer ${API_TOKEN}` },

  },

);

const { webSocketDebuggerUrl } = await response.json();


// Connect Puppeteer to the session

const browser = await puppeteer.connect({

  browserWSEndpoint: webSocketDebuggerUrl,

  headers: { Authorization: `Bearer ${API_TOKEN}` },

});


const page = await browser.newPage();


// Add a custom font — same as with Workers Bindings

await page.addStyleTag({

  content: `

    @font-face {

      font-family: 'CustomFont';

      src: url('https://your-cdn.com/fonts/MyFont.woff2') format('woff2');

      font-weight: normal;

      font-style: normal;

    }


    body {

      font-family: 'CustomFont', sans-serif;

    }

  `,

});


// Take a screenshot, generate a PDF, etc.

await page.goto("https://example.com");


browser.disconnect();


```

Explain Code

## Quick Actions

When using [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/), you can load custom fonts by including the `addStyleTag` parameter in your request body. This works with both the [screenshot](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/) and [PDF](https://developers.cloudflare.com/browser-run/quick-actions/pdf-endpoint/) endpoints.

### From a CDN URL

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/",

    "addStyleTag": [

      {

        "content": "@font-face { font-family: '\''CustomFont'\''; src: url('\''https://your-cdn.com/fonts/MyFont.woff2'\'') format('\''woff2'\''); font-weight: normal; font-style: normal; } body { font-family: '\''CustomFont'\'', sans-serif; }"

      }

    ]

  }' \

  --output "screenshot.png"


```

Explain Code

### Base64-encoded

Terminal window

```

curl -X POST 'https://api.cloudflare.com/client/v4/accounts/<accountId>/browser-rendering/screenshot' \

  -H 'Authorization: Bearer <apiToken>' \

  -H 'Content-Type: application/json' \

  -d '{

    "url": "https://example.com/",

    "addStyleTag": [

      {

        "content": "@font-face { font-family: '\''CustomFont'\''; src: url('\''data:font/woff2;base64,<BASE64_STRING>'\'') format('\''woff2'\''); font-weight: normal; font-style: normal; } body { font-family: '\''CustomFont'\'', sans-serif; }"

      }

    ]

  }' \

  --output "screenshot.png"


```

Explain Code

For more details on using `addStyleTag` with Quick Actions, refer to [Customize CSS and embed custom JavaScript](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/#customize-css-and-embed-custom-javascript).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/features/","name":"Features"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/features/custom-fonts/","name":"Custom fonts"}}]}
```

---

---
title: Human in the Loop
description: Temporarily hand off browser control to a human operator for authentication, sensitive actions, or tasks that are difficult to fully automate.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/features/human-in-the-loop.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Human in the Loop

Some browser automation workflows require manual intervention. A login page may need multi-factor authentication, a form may require sensitive credentials you do not want to pass to an automation script, or a task may be too complex to fully automate. Human in the Loop lets a human step into a live browser session through [Live View](https://developers.cloudflare.com/browser-run/features/live-view/) to handle what automation cannot, then hand control back to the script.

## How it works

Human in the Loop works with any [Browser Session](https://developers.cloudflare.com/browser-run/#integration-methods) and uses [Live View](https://developers.cloudflare.com/browser-run/features/live-view/) to give humans access:

1. Your automation script navigates to a page that needs human input.
2. The script retrieves the [Live View](https://developers.cloudflare.com/browser-run/features/live-view/) URL from the session's target list and shares it with a human operator (for example, by sending it via Slack, email, or displaying it in a user interface).
3. The human operator opens the Live View URL and completes the required action (logging in, solving a CAPTCHA, entering sensitive data, etc.).
4. The automation script detects that the human is done (for example, by waiting for a navigation event or polling for a page element) and resumes.

A more structured handoff flow where the agent can signal that it needs help and notify a human is coming soon.

## Example: login with human assistance

This example uses [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) connected to Browser Run via the [CDP](https://developers.cloudflare.com/browser-run/cdp/) endpoints. The script navigates to a login page, shares a Live View URL for a human to enter credentials, then continues the automation after login completes.

JavaScript

```

import puppeteer from "puppeteer-core";


const ACCOUNT_ID = "<your-account-id>";

const API_TOKEN = "<your-api-token>";


// Create a browser session via CDP

const response = await fetch(

  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-rendering/devtools/browser?keep_alive=600000&targets=true`,

  {

    method: "POST",

    headers: { Authorization: `Bearer ${API_TOKEN}` },

  },

);

const { webSocketDebuggerUrl, targets } = await response.json();

const liveUrl = targets[0].devtoolsFrontendUrl;


// Connect Puppeteer to the session

const browser = await puppeteer.connect({

  browserWSEndpoint: webSocketDebuggerUrl,

  headers: { Authorization: `Bearer ${API_TOKEN}` },

});


const page = await browser.newPage();

await page.goto("https://example.com/login");


// Share the Live View URL with the human operator (for example, send it via Slack, email, or display it in a UI)

console.log(`Human input needed. Open this URL: ${liveUrl}`);


// Wait for the human to complete login (5 minute timeout — the script will continue after this period)

await page.waitForNavigation({ waitUntil: "networkidle0", timeout: 300000 });


// Login complete, continue automation

const cookies = await page.cookies();

console.log("Login complete. Continuing automation...");


await page.goto("https://example.com/dashboard");

const content = await page.content();


browser.disconnect();


```

Explain Code

The Live View URL is valid for five minutes from when it was generated. If the URL expires before the human operator opens it, list the targets again to get a fresh URL.

## Use cases

* **Authentication flows**: Login pages with MFA, SSO, or CAPTCHA that cannot be bypassed programmatically
* **Sensitive data entry**: Forms requiring credentials or personal information you do not want to pass to an automation script
* **Complex interactions**: One-off tasks that are too difficult or not worth fully automating, such as configuring a dashboard or approving a workflow
* **Verification steps**: Confirming an order, reviewing generated content, or approving an action before the script proceeds

Bot detection

Browser Run requests are [always identified as bot traffic](https://developers.cloudflare.com/browser-run/faq/#will-browser-run-be-detected-by-bot-management). Even with a human controlling the session, some third-party services may still block the request.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/features/","name":"Features"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/features/human-in-the-loop/","name":"Human in the Loop"}}]}
```

---

---
title: Live View
description: View and interact with remote Browser Run sessions in real time using the hosted DevTools UI or native Chrome DevTools.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/features/live-view.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Live View

Live View lets you see and interact with a remote Browser Run session in real time. This is useful for debugging automation scripts, monitoring what a browser is doing, or manually stepping in when a task requires human intervention (see [Human in the Loop](https://developers.cloudflare.com/browser-run/features/human-in-the-loop/)).

Live View is available for any [Browser Session](https://developers.cloudflare.com/browser-run/#integration-methods), including sessions created with [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), or the [CDP](https://developers.cloudflare.com/browser-run/cdp/) endpoints.

## How to access Live View

There are three ways to access Live View: through the Cloudflare dashboard, via the hosted user interface (UI) at `live.browser.run`, or using native Chrome DevTools.

### Cloudflare dashboard

In the Cloudflare dashboard, go to the **Browser Run** page and select the **Live Sessions** tab. This shows all active browser sessions in your account. Expand a session to see its tabs, then select **Open** to open the Live View for that tab.

[ Go to **Browser Run** ](https://dash.cloudflare.com/?to=/:account/workers/browser-run) 

### Hosted UI (any browser)

When you create a session or list targets through the [CDP](https://developers.cloudflare.com/browser-run/cdp/) endpoints, the API response includes a `devtoolsFrontendUrl` for each target (tab). Open this URL in any browser to load the DevTools UI hosted at `live.browser.run`, which streams the remote session to your browser.

The hosted UI supports two viewing modes, controlled by the `mode` parameter in the URL:

| Mode      | URL pattern                                            | Description                                                 |
| --------- | ------------------------------------------------------ | ----------------------------------------------------------- |
| Tab       | https://live.browser.run/ui/view?mode=tab&wss=...      | Standalone page view                                        |
| Inspector | https://live.browser.run/ui/view?mode=devtools&wss=... | DevTools inspector panel (Elements, Console, Network, etc.) |

### Native Chrome DevTools (Chrome only)

Because Browser Run speaks standard CDP, you can connect Chrome's built-in DevTools directly to a remote session. Replace the `https://live.browser.run/ui/inspector?wss=` prefix in the `devtoolsFrontendUrl` with the `devtools://` protocol:

```

devtools://devtools/bundled/inspector.html?wss=live.browser.run/api/devtools/browser/SESSION_ID/page/TARGET_ID?jwt=...


```

Paste this URL into Chrome's address bar to connect native DevTools to the remote browser session. You will get the same DevTools interface you use for local debugging. The `devtools://` protocol is Chrome-only and limited to inspector viewing mode.

URL validity

The `devtoolsFrontendUrl` is valid for five minutes from when it was generated. If you do not open the URL within this timeframe, list the targets again to get a fresh URL. Once the DevTools connection is established, it remains active as long as the browser session is alive.

## View a new session

1. Create a browser session with `targets=true` to include target URLs in the response:

Terminal window

```

curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/browser-rendering/devtools/browser?keep_alive=600000&targets=true" \

  --request POST \

  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"


```

```

{

  "sessionId": "1909cef7-23e8-4394-bc31-27404bf4348f",

  "targets": [

    {

      "description": "",

      "devtoolsFrontendUrl": "https://live.browser.run/ui/inspector?wss=live.browser.run/api/devtools/browser/1909cef7-.../page/8E598E99...?jwt=...",

      "id": "8E598E996530FB09E46A22B8B7754F7F",

      "title": "about:blank",

      "type": "page",

      "url": "about:blank",

      "webSocketDebuggerUrl": "wss://live.browser.run/api/devtools/browser/1909cef7-.../page/8E598E99...?jwt=..."

    }

  ],

  "webSocketDebuggerUrl": "wss://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/devtools/browser/1909cef7-..."

}


```

Explain Code

1. Copy the `devtoolsFrontendUrl` from `targets[0]` and open it in your browser. You now have a live, interactive view of the remote browser session.

## View an existing session

If you have a running session and want to connect to it:

1. List your active sessions:  
Terminal window  
```  
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/browser-rendering/devtools/session" \  
  --request GET \  
  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"  
```
2. Using the session ID, list the targets in that session:  
Terminal window  
```  
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/browser-rendering/devtools/browser/$SESSION_ID/json/list" \  
  --request GET \  
  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"  
```  
```  
[  
  {  
    "id": "110850A800BDB8B593CDDA30676635CF",  
    "type": "page",  
    "url": "https://example.com",  
    "title": "Example Domain",  
    "description": "",  
    "devtoolsFrontendUrl": "https://live.browser.run/ui/view?wss=live.browser.run/api/devtools/browser/28d75446-.../page/110850A8...?jwt=...",  
    "webSocketDebuggerUrl": "wss://live.browser.run/api/devtools/browser/28d75446-.../page/110850A8...?jwt=..."  
  }  
]  
```  
Explain Code
3. Copy the `devtoolsFrontendUrl` and open it in your browser.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/features/","name":"Features"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/features/live-view/","name":"Live View"}}]}
```

---

---
title: Reuse sessions
description: By default, each Browser Sessions request launches a new browser instance. Reusing sessions eliminates cold-start time and improves performance by reconnecting to an existing browser instead of launching a new one.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/features/reuse-sessions.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Reuse sessions

By default, each Browser Sessions request launches a new browser instance. Reusing sessions eliminates cold-start time and improves performance by reconnecting to an existing browser instead of launching a new one.

This feature applies to Browser Sessions ([Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), and [CDP](https://developers.cloudflare.com/browser-run/cdp/)). [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) handle session lifecycle automatically.

There are two approaches to reusing sessions:

* **Disconnect and reconnect** (covered in this page): Use `browser.disconnect()` instead of `browser.close()` to keep the browser alive, then reconnect to it on the next request. Best for stateless workloads where any available browser session will do.
* **[Durable Objects](https://developers.cloudflare.com/browser-run/how-to/browser-run-with-do/)**: Persist a long-running browser inside a Durable Object for stateful session management. Best when you need to maintain state across requests or route specific users to specific browser instances.

## 1\. Create a Worker project

[Cloudflare Workers](https://developers.cloudflare.com/workers/) provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure. Your Worker application is a container to interact with a headless browser to do actions, such as taking screenshots.

Create a new Worker project named `browser-worker` by running:

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- browser-worker
```

```
yarn create cloudflare browser-worker
```

```
pnpm create cloudflare@latest browser-worker
```

For setup, select the following options:

* For _What would you like to start with?_, choose `Hello World example`.
* For _Which template would you like to use?_, choose `Worker only`.
* For _Which language do you want to use?_, choose `TypeScript`.
* For _Do you want to use git for version control?_, choose `Yes`.
* For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying).

## 2\. Install Puppeteer

In your `browser-worker` directory, install Cloudflare's [fork of Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/):

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/puppeteer
```

```
yarn add -D @cloudflare/puppeteer
```

```
pnpm add -D @cloudflare/puppeteer
```

```
bun add -d @cloudflare/puppeteer
```

## 3\. Configure the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/)

Note

Your Worker configuration must include the `nodejs_compat` compatibility flag and a `compatibility_date` of 2025-09-15 or later.

* [  wrangler.jsonc ](#tab-panel-3546)
* [  wrangler.toml ](#tab-panel-3547)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "browser-worker",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-16",

  "compatibility_flags": ["nodejs_compat"],

  "browser": {

    "binding": "MYBROWSER",

  },

}


```

Explain Code

TOML

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "browser-worker"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-16"

compatibility_flags = [ "nodejs_compat" ]


[browser]

binding = "MYBROWSER"


```

## 4\. Code

The script below starts by fetching the current running sessions. If there are any that do not already have a worker connection, it picks a random session ID and attempts to connect (`puppeteer.connect(..)`) to it. If that fails or there were no running sessions to start with, it launches a new browser session (`puppeteer.launch(..)`). Then, it goes to the website and fetches the dom. Once that is done, it disconnects (`browser.disconnect()`), making the connection available to other workers.

Take into account that if the browser is idle, i.e. does not get any command, for more than the current [limit](https://developers.cloudflare.com/browser-run/limits/), it will close automatically, so you must have enough requests per minute to keep it alive.

* [  JavaScript ](#tab-panel-3548)
* [  TypeScript ](#tab-panel-3549)

JavaScript

```

import puppeteer from "@cloudflare/puppeteer";


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    let reqUrl = url.searchParams.get("url") || "https://example.com";

    reqUrl = new URL(reqUrl).toString(); // normalize


    // Pick random session from open sessions

    let sessionId = await this.getRandomSession(env.MYBROWSER);

    let browser, launched;

    if (sessionId) {

      try {

        browser = await puppeteer.connect(env.MYBROWSER, sessionId);

      } catch (e) {

        // another worker may have connected first

        console.log(`Failed to connect to ${sessionId}. Error ${e}`);

      }

    }

    if (!browser) {

      // No open sessions, launch new session

      browser = await puppeteer.launch(env.MYBROWSER);

      launched = true;

    }


    sessionId = browser.sessionId(); // get current session id


    // Do your work here

    const page = await browser.newPage();

    const response = await page.goto(reqUrl);

    const html = await response.text();


    // All work done, so free connection (IMPORTANT!)

    browser.disconnect();


    return new Response(

      `${launched ? "Launched" : "Connected to"} ${sessionId} \n-----\n` + html,

      {

        headers: {

          "content-type": "text/plain",

        },

      },

    );

  },


  // Pick random free session

  // Other custom logic could be used instead

  async getRandomSession(endpoint) {

    const sessions = await puppeteer.sessions(endpoint);

    console.log(`Sessions: ${JSON.stringify(sessions)}`);

    const sessionsIds = sessions

      .filter((v) => {

        return !v.connectionId; // remove sessions with workers connected to them

      })

      .map((v) => {

        return v.sessionId;

      });

    if (sessionsIds.length === 0) {

      return;

    }


    const sessionId =

      sessionsIds[Math.floor(Math.random() * sessionsIds.length)];


    return sessionId;

  },

};


```

Explain Code

TypeScript

```

import puppeteer from "@cloudflare/puppeteer";


interface Env {

  MYBROWSER: Fetcher;

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);

    let reqUrl = url.searchParams.get("url") || "https://example.com";

    reqUrl = new URL(reqUrl).toString(); // normalize


    // Pick random session from open sessions

    let sessionId = await this.getRandomSession(env.MYBROWSER);

    let browser, launched;

    if (sessionId) {

      try {

        browser = await puppeteer.connect(env.MYBROWSER, sessionId);

      } catch (e) {

        // another worker may have connected first

        console.log(`Failed to connect to ${sessionId}. Error ${e}`);

      }

    }

    if (!browser) {

      // No open sessions, launch new session

      browser = await puppeteer.launch(env.MYBROWSER);

      launched = true;

    }


    sessionId = browser.sessionId(); // get current session id


    // Do your work here

    const page = await browser.newPage();

    const response = await page.goto(reqUrl);

    const html = await response!.text();


    // All work done, so free connection (IMPORTANT!)

    browser.disconnect();


    return new Response(

      `${launched ? "Launched" : "Connected to"} ${sessionId} \n-----\n` + html,

      {

        headers: {

          "content-type": "text/plain",

        },

      },

    );

  },


  // Pick random free session

  // Other custom logic could be used instead

  async getRandomSession(endpoint: puppeteer.BrowserWorker): Promise<string> {

    const sessions: puppeteer.ActiveSession[] =

      await puppeteer.sessions(endpoint);

    console.log(`Sessions: ${JSON.stringify(sessions)}`);

    const sessionsIds = sessions

      .filter((v) => {

        return !v.connectionId; // remove sessions with workers connected to them

      })

      .map((v) => {

        return v.sessionId;

      });

    if (sessionsIds.length === 0) {

      return;

    }


    const sessionId =

      sessionsIds[Math.floor(Math.random() * sessionsIds.length)];


    return sessionId!;

  },

};


```

Explain Code

Besides `puppeteer.sessions()`, we have added other methods to facilitate [Session Management](https://developers.cloudflare.com/browser-run/puppeteer/#session-management).

## 5\. Test

Run `npx wrangler dev` to test your Worker locally.

Use real headless browser during local development

To interact with a real headless browser during local development, set `"remote" : true` in the Browser binding configuration. Learn more in our [remote bindings documentation](https://developers.cloudflare.com/workers/development-testing/#remote-bindings).

To test go to the following URL:

```

<LOCAL_HOST_URL>/?url=https://example.com


```

## 6\. Deploy

Run `npx wrangler deploy` to deploy your Worker to the Cloudflare global network and then to go to the following URL:

```

<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/?url=https://example.com


```

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/features/","name":"Features"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/features/reuse-sessions/","name":"Reuse sessions"}}]}
```

---

---
title: Session recording
description: Record and replay Browser Run sessions to visually debug browser automation scripts.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/features/session-recording.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Session recording

Beta 

When browser automation fails or behaves unexpectedly, it can be difficult to understand what happened. Session recording captures DOM changes, mouse and keyboard events, and page navigation as structured JSON events — not a video — so it is lightweight and easy to inspect. Recordings are powered by [rrweb ↗](https://github.com/rrweb-io/rrweb) and are opt-in per session.

## Enable session recording

Pass `recording: true` to `puppeteer.launch()` or `playwright.launch()`:

* [ Puppeteer ](#tab-panel-3550)
* [ Playwright ](#tab-panel-3551)

TypeScript

```

import puppeteer from "@cloudflare/puppeteer";


interface Env {

  MYBROWSER: Fetcher;

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const browser = await puppeteer.launch(env.MYBROWSER, { recording: true });

    const page = await browser.newPage();


    await page.goto("https://example.com");

    // ... your automation steps ...


    const sessionId = browser.sessionId();

    await browser.close();


    return new Response(`Session recorded: ${sessionId}`);

  },

};


```

Explain Code

TypeScript

```

import { launch } from "@cloudflare/playwright";


interface Env {

  MYBROWSER: Fetcher;

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const browser = await launch(env.MYBROWSER, { recording: true });

    const page = await browser.newPage();


    await page.goto("https://example.com");

    // ... your automation steps ...


    const sessionId = browser.sessionId();

    await browser.close();


    return new Response(`Session recorded: ${sessionId}`);

  },

};


```

Explain Code

Note

The recording is finalized when the browser session closes, whether you call `browser.close()` explicitly, the session reaches its idle timeout, or the Worker terminates for any other reason. The recording is not available until after the session ends.

## Enable with CDP endpoint

When connecting to Browser Run from any environment using the [CDP endpoint](https://developers.cloudflare.com/browser-run/cdp/), add `recording=true` as a query parameter to the WebSocket URL:

```

wss://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-rendering/devtools/browser?recording=true&keep_alive=600000


```

For example, to enable session recording in an MCP client, add `recording=true` to the `--wsEndpoint` URL in your client configuration:

```

{

  "mcpServers": {

    "browser-rendering": {

      "command": "npx",

      "args": [

        "-y",

        "chrome-devtools-mcp@latest",

        "--wsEndpoint=wss://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-rendering/devtools/browser?recording=true&keep_alive=600000",

        "--wsHeaders={\"Authorization\":\"Bearer <API_TOKEN>\"}"

      ]

    }

  }

}


```

Explain Code

For other MCP clients and CDP usage with Puppeteer or Playwright, refer to the [CDP documentation](https://developers.cloudflare.com/browser-run/cdp/).

Note

The recording is only available after the browser session closes. CDP sessions typically use `keep_alive` to stay open between commands. The browser will close automatically when it has been idle for the `keep_alive` duration. You can also close it explicitly with `browser.close()`.

## View recordings

After a session closes, its recording is available in the Cloudflare dashboard under **Browser Run** \> **Runs**. Select a session to open the recording viewer, where you can scrub through the timeline and replay what happened during the session.

[ Go to **Browser Run Logs** ](https://dash.cloudflare.com/?to=/:account/workers/browser-run/logs) 

## Retrieve a recording via API

You can also retrieve a recording programmatically using the session ID. Use `browser.sessionId()` to capture the session ID before closing the browser, then pass it to the recordings endpoint.

Terminal window

```

curl https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-rendering/recording/<SESSION_ID> \

  -H "Authorization: Bearer <API_TOKEN>"


```

A successful response looks similar to the following:

```

{

  "sessionId": "e26d4660-5b78-4761-b82f-c6b5bad5a925",

  "duration": 4380,

  "events": {

    "target-1": []

  }

}


```

## Replay a recording

The `events` values in the API response are standard rrweb event arrays. You can pass them directly to [rrweb-player ↗](https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-player) to self-host a replay UI with a timeline scrubber and playback controls.

## Limits

* Recordings are retained for 30 days after the session ends and automatically deleted.
* Recording is opt-in. It is not enabled by default.
* Session recording is available with Browser Sessions via `launch()` and the [CDP endpoint](https://developers.cloudflare.com/browser-run/cdp/). It is not available with Quick Actions.
* The minimum recording duration is 1 second. Sessions shorter than 1 second will not produce a viewable recording.
* The maximum recording duration is 2 hours.

## rrweb limitations

Session recording uses [rrweb ↗](https://github.com/rrweb-io/rrweb), which records DOM state and events rather than pixels. This approach is lightweight but has the following limitations:

* **Canvas elements** — The content of `<canvas>` elements is not captured. The element itself appears in the recording as a blank placeholder.
* **Cross-origin iframes** — Content inside cross-origin `<iframe>` elements is not recorded. Same-origin iframes are recorded normally.
* **Video and audio** — The DOM structure of `<video>` and `<audio>` elements is captured, but media playback state and content are not.
* **WebGL** — WebGL rendering is not captured.
* **Input fields** — The content of all input fields is masked by default and will not be visible in the replay.
* **Large or complex pages** — Pages with frequent DOM mutations (for example, pages with real-time data feeds or heavy animations) can generate a high volume of events, which increases the size of the recording.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/features/","name":"Features"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/features/session-recording/","name":"Session recording"}}]}
```

---

---
title: WebMCP
description: Use WebMCP to let AI agents discover and execute structured tools exposed by websites, replacing fragile screenshot-analyze-click loops with direct function calls.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/features/webmcp.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# WebMCP

[WebMCP ↗](https://developer.chrome.com/blog/webmcp-epp) (Web Model Context Protocol) is a browser API that lets websites expose structured tools for AI agents to discover and execute directly. Instead of slow screenshot-analyze-click loops, agents can call website functions like `searchFlights()` or `bookTicket()` with typed parameters, making browser automation faster, more reliable, and less fragile.

## Get started

### Manual testing with DevTools

#### 1\. Start a Lab session and open DevTools

WebMCP is currently available in Chrome beta, so it requires a lab session. Browser Run has an experimental pool with browser instances running Chrome beta so you can test emerging browser features before they reach stable Chrome. Your production workloads on the [standard pool](https://developers.cloudflare.com/browser-run/#key-features) remain on a stable version of Chrome.

Use the new `wrangler browser` command to acquire a lab browser session:

Terminal window

```

# make sure you have the latest version of wrangler

npm i -g wrangler@latest


# create a lab browser session with 5 minute keep-alive

wrangler browser create --lab --keepAlive 300


```

It will open a live view of your browser session.

#### 2\. Interact with the page

You can now interact with the page as you would in a regular browser.

1. Go to one of the sites listed in the [WebMCP documentation ↗](https://github.com/GoogleChromeLabs/webmcp-tools/?tab=readme-ov-file#demos). The following instructions are based on the [L'Atelier Hotel Chain ↗](https://github.com/GoogleChromeLabs/webmcp-tools/tree/main/demos/hotel-chain) demo.
2. Open the [hotel chain demo URL ↗](https://googlechromelabs.github.io/webmcp-tools/demos/hotel-chain/) and then, in the **Console** tab, run the following JavaScript statement to list the available tools:  
JavaScript  
```  
navigator.modelContextTesting.listTools();  
```

You should get a result similar to the following:

```

[

  {

    "description": "View the details of a specific hotel by name or id",

    "inputSchema": "...",

    "name": "view_hotel"

  },

  {

    "description": "Find me a hotel in a specific location",

    "inputSchema": "...",

    "name": "search_location"

  },

  {

    "description": "Look up specific amenity or policy details for a hotel",

    "inputSchema": "...",

    "name": "lookup_amenity"

  }

]


```

Explain Code

The list of tools changes depending on the website you are visiting and the actions you have performed on the page.

For instance, on the hotel chain website, after executing the `search_location` tool:

JavaScript

```

await navigator.modelContextTesting.executeTool(

  "search_location",

  JSON.stringify({ query: "Paris" }),

);


```

The page redirects to the search results, and a new tool `filter_search_results` becomes available.

You can call it to filter by amenities. For example, if you want to eat a good croissant in the morning:

JavaScript

```

await navigator.modelContextTesting.executeTool(

  "filter_search_results",

  JSON.stringify({ amenities: ["breakfast"] }),

);


```

You will get a list of filtered results, where you can pick the best option for your needs. Once you select a hotel, you can use the `start_booking` tool:

JavaScript

```

await navigator.modelContextTesting.executeTool(

  "start_booking",

  JSON.stringify({}),

);


```

Then, you can complete the booking:

JavaScript

```

await navigator.modelContextTesting.executeTool(

  "complete_booking",

  JSON.stringify({

    firstName: "James",

    lastName: "Bond",

    email: "james.bond@mi6.gov.uk",

  }),

);


```

Note that the `complete_booking` tool requires human confirmation. The tool waits until you select the **Confirm Reservation** button in the browser. This is an example of human-in-the-loop (HITL): WebMCP tools can pause execution and wait for user interaction before completing sensitive actions.

After you select **Confirm Reservation**, you will get a confirmation message and the booking is complete.

### Using an AI Agent

#### 1\. Configure Chrome DevTools MCP

[Chrome DevTools MCP ↗](https://github.com/ChromeDevTools/chrome-devtools-mcp) allows AI agents to control a browser via CDP. Configure your MCP client (such as Claude Desktop, Claude Code, Cursor, or OpenCode) with the following settings. For more details on MCP client configuration, refer to [Using with MCP clients](https://developers.cloudflare.com/browser-run/cdp/mcp-clients/).

```

{

  "browser-rendering-cdp": {

    "command": [

      "npx",

      "-y",

      "chrome-devtools-mcp@latest",

      "--wsEndpoint=wss://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-rendering/devtools/browser?keep_alive=600000&lab=true",

      "--wsHeaders={\"Authorization\":\"Bearer <CLOUDFLARE_API_TOKEN>\"}"

    ]

  }

}


```

Explain Code

Replace `<ACCOUNT_ID>` with your Cloudflare account ID and `<CLOUDFLARE_API_TOKEN>` with your API token. The `lab=true` parameter ensures the agent connects to a lab session with WebMCP enabled.

#### 2\. (Optional) Add a WebMCP Skill

To help your agent understand how to work with WebMCP, add the following skill to your agent configuration:

```

---

name: webmcp

description: Interact with WebMCP-enabled websites using Browser Run lab sessions

---


# WebMCP Interaction Skill


Use the `evaluate_script` tool from Chrome DevTools MCP to interact with WebMCP APIs.


**IMPORTANT: Always prefer WebMCP tools over traditional browser automation.** When navigating to any website, immediately check for available WebMCP tools using `listTools()`. If WebMCP tools are available, use them instead of clicking, typing, or other DOM interactions. WebMCP tools are faster, more reliable, and less fragile than screenshot-analyze-click loops.


## Workflow


1. **Navigate** to a site using `navigate_page`

2. **Always list tools first** to check for WebMCP support—do this on every page load

3. **Prefer WebMCP tools** over clicking/typing when tools are available

4. **Execute tools** to perform actions directly

5. **Re-list tools** after each action (tools change based on page state)

6. **Check `inputSchema`** in each tool to understand required parameters

7. **Fall back to DOM interaction** only when no relevant WebMCP tools exist


## Commands


**List available tools:**


```js

evaluate_script({

  function: "async () => await navigator.modelContextTesting.listTools()",

});

```


**Execute a tool:**


```js

evaluate_script({

  function:

    "async () => await navigator.modelContextTesting.executeTool('tool_name', JSON.stringify({ param: 'value' }))",

});

```


```

Explain Code

#### 3\. Interact with WebMCP sites

Once configured, your AI agent can navigate to WebMCP-enabled sites and use WebMCP tools. Here is an example conversation:

**You:** Go to [https://googlechromelabs.github.io/webmcp-tools/demos/hotel-chain/ ↗](https://googlechromelabs.github.io/webmcp-tools/demos/hotel-chain/) and find me a hotel in Paris with breakfast. Use WebMCP tools when available.

_Agent navigates to the site, lists WebMCP tools, executes `searchlocation` with "Paris", then `filtersearchresults` with breakfast amenity, and presents the results._

**You:** Pick the first one and book it for Bond, James Bond ([james.bond@mi6.gov.uk](mailto:james.bond@mi6.gov.uk)).

_Agent clicks the hotel, executes `startbooking`, then `completebooking` with the provided guest details._

#### 4\. (Optional) Open DevTools to watch the agent

Some WebMCP tools require human confirmation before completing sensitive actions. For example, `complete_booking` waits for you to select **Confirm** before finalizing a reservation. To interact with these human-in-the-loop (HITL) prompts, you need to open the browser's live view.

Once the agent has started a session, list active sessions to get the session ID:

Terminal window

```

wrangler browser list


```

Then, use the session ID from the previous response to open the browser's live view:

Terminal window

```

wrangler browser view $SESSION_ID


```

You can now view the live browser session and interact with it.

## Limitations

* Lab sessions use Chrome 146 beta, which may have stability issues.
* WebMCP APIs (`navigator.modelContext`, `navigator.modelContextTesting`) only work in lab sessions.
* Lab sessions count against your regular rate limits.
* The `lab` parameter is not yet supported in `@cloudflare/puppeteer` or `@cloudflare/playwright`. Acquire the session manually and connect with `sessionId`.

## More resources

* [Chrome WebMCP blog post ↗](https://developer.chrome.com/blog/webmcp-epp)
* [WebMCP specification ↗](https://github.com/webmachinelearning/webmcp)
* [Browser Run documentation](https://developers.cloudflare.com/browser-run/)

## Troubleshooting

If you have questions or encounter an error, see the [Browser Run FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-run/faq/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/features/","name":"Features"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/features/webmcp/","name":"WebMCP"}}]}
```

---

---
title: Use Browser Run with AI
description: The ability to browse websites can be crucial when building workflows with AI. Here, we provide an example where we use Browser Run to visit https://labs.apnic.net/ and then, using a machine learning model available in Workers AI, extract the first post as JSON with a specified schema.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ AI ](https://developers.cloudflare.com/search/?tags=AI)[ LLM ](https://developers.cloudflare.com/search/?tags=LLM) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/how-to/ai.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Use Browser Run with AI

The ability to browse websites can be crucial when building workflows with AI. Here, we provide an example where we use Browser Run to visit `https://labs.apnic.net/` and then, using a machine learning model available in [Workers AI](https://developers.cloudflare.com/workers-ai/), extract the first post as JSON with a specified schema.

## Prerequisites

1. Use the `create-cloudflare` CLI to generate a new Hello World Cloudflare Worker script:

Terminal window

```

npm create cloudflare@latest -- browser-worker


```

1. Install `@cloudflare/puppeteer`, which allows you to control the Browser Run instance:

Terminal window

```

npm i @cloudflare/puppeteer


```

1. Install `zod` so we can define our output format and `zod-to-json-schema` so we can convert it into a JSON schema format:

Terminal window

```

npm i zod

npm i zod-to-json-schema


```

1. Activate the nodejs compatibility flag and add your Browser Run binding to your new Wrangler configuration:

* [  wrangler.jsonc ](#tab-panel-3556)
* [  wrangler.toml ](#tab-panel-3557)

JSONC

```

{

  "compatibility_flags": [

    "nodejs_compat"

  ]

}


```

TOML

```

compatibility_flags = [ "nodejs_compat" ]


```

* [  wrangler.jsonc ](#tab-panel-3558)
* [  wrangler.toml ](#tab-panel-3559)

JSONC

```

{

  "browser": {

    "binding": "MY_BROWSER"

  }

}


```

TOML

```

[browser]

binding = "MY_BROWSER"


```

1. In order to use [Workers AI](https://developers.cloudflare.com/workers-ai/), you need to get your [Account ID and API token](https://developers.cloudflare.com/workers-ai/get-started/rest-api/#1-get-api-token-and-account-id).  
Once you have those, create a [.dev.vars](https://developers.cloudflare.com/workers/configuration/environment-variables/#add-environment-variables-via-wrangler) file and set them there:  
```  
ACCOUNT_ID=  
API_TOKEN=  
```

We use `.dev.vars` here since it's only for local development, otherwise you'd use [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/).

## Load the page using Browser Run

In the code below, we launch a browser using `await puppeteer.launch(env.MY_BROWSER)`, extract the rendered text and close the browser.

Then, with the user prompt, the desired output schema and the rendered text, prepare a prompt to send to the LLM.

Replace the contents of `src/index.ts` with the following skeleton script:

TypeScript

```

import { z } from "zod";

import puppeteer from "@cloudflare/puppeteer";

import zodToJsonSchema from "zod-to-json-schema";


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    if (url.pathname != "/") {

      return new Response("Not found");

    }


    // Your prompt and site to scrape

    const userPrompt = "Extract the first post only.";

    const targetUrl = "https://labs.apnic.net/";


    // Launch browser

    const browser = await puppeteer.launch(env.MY_BROWSER);

    const page = await browser.newPage();

    await page.goto(targetUrl);


    // Get website text

    const renderedText = await page.evaluate(() => {

      // @ts-ignore js code to run in the browser context

      const body = document.querySelector("body");

      return body ? body.innerText : "";

    });

    // Close browser since we no longer need it

    await browser.close();


    // define your desired json schema

    const outputSchema = zodToJsonSchema(

      z.object({ title: z.string(), url: z.string(), date: z.string() }),

    );


    // Example prompt

    const prompt = `

    You are a sophisticated web scraper. You are given the user data extraction goal and the JSON schema for the output data format.

    Your task is to extract the requested information from the text and output it in the specified JSON schema format:


        ${JSON.stringify(outputSchema)}


    DO NOT include anything else besides the JSON output, no markdown, no plaintext, just JSON.


    User Data Extraction Goal: ${userPrompt}


    Text extracted from the webpage: ${renderedText}`;


    // TODO call llm

    //const result = await getLLMResult(env, prompt, outputSchema);

    //return Response.json(result);

  },

} satisfies ExportedHandler<Env>;


```

Explain Code

## Call an LLM

Having the webpage text, the user's goal and output schema, we can now use an LLM to transform it to JSON according to the user's request.

The example below uses `@hf/thebloke/deepseek-coder-6.7b-instruct-awq` but other [models](https://developers.cloudflare.com/workers-ai/models/) or services like OpenAI, could be used with minimal changes:

TypeScript

```

async function getLLMResult(env, prompt: string, schema?: any) {

  const model = "@hf/thebloke/deepseek-coder-6.7b-instruct-awq";

  const requestBody = {

    messages: [

      {

        role: "user",

        content: prompt,

      },

    ],

  };

  const aiUrl = `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/ai/run/${model}`;


  const response = await fetch(aiUrl, {

    method: "POST",

    headers: {

      "Content-Type": "application/json",

      Authorization: `Bearer ${env.API_TOKEN}`,

    },

    body: JSON.stringify(requestBody),

  });

  if (!response.ok) {

    console.log(JSON.stringify(await response.text(), null, 2));

    throw new Error(`LLM call failed ${aiUrl} ${response.status}`);

  }


  // process response

  const data = await response.json();

  const text = data.result.response || "";

  const value = (text.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || [

    null,

    text,

  ])[1];

  try {

    return JSON.parse(value);

  } catch (e) {

    console.error(`${e} . Response: ${value}`);

  }

}


```

Explain Code

If you want to use Browser Run with OpenAI instead you'd just need to change the `aiUrl` endpoint and `requestBody` (or check out the [llm-scraper-worker ↗](https://www.npmjs.com/package/llm-scraper-worker) package).

## Conclusion

The full Worker script now looks as follows:

TypeScript

```

import { z } from "zod";

import puppeteer from "@cloudflare/puppeteer";

import zodToJsonSchema from "zod-to-json-schema";


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    if (url.pathname != "/") {

      return new Response("Not found");

    }


    // Your prompt and site to scrape

    const userPrompt = "Extract the first post only.";

    const targetUrl = "https://labs.apnic.net/";


    // Launch browser

    const browser = await puppeteer.launch(env.MY_BROWSER);

    const page = await browser.newPage();

    await page.goto(targetUrl);


    // Get website text

    const renderedText = await page.evaluate(() => {

      // @ts-ignore js code to run in the browser context

      const body = document.querySelector("body");

      return body ? body.innerText : "";

    });

    // Close browser since we no longer need it

    await browser.close();


    // define your desired json schema

    const outputSchema = zodToJsonSchema(

      z.object({ title: z.string(), url: z.string(), date: z.string() }),

    );


    // Example prompt

    const prompt = `

    You are a sophisticated web scraper. You are given the user data extraction goal and the JSON schema for the output data format.

    Your task is to extract the requested information from the text and output it in the specified JSON schema format:


        ${JSON.stringify(outputSchema)}


    DO NOT include anything else besides the JSON output, no markdown, no plaintext, just JSON.


    User Data Extraction Goal: ${userPrompt}


    Text extracted from the webpage: ${renderedText}`;


    // call llm

    const result = await getLLMResult(env, prompt, outputSchema);

    return Response.json(result);

  },

} satisfies ExportedHandler<Env>;


async function getLLMResult(env, prompt: string, schema?: any) {

  const model = "@hf/thebloke/deepseek-coder-6.7b-instruct-awq";

  const requestBody = {

    messages: [

      {

        role: "user",

        content: prompt,

      },

    ],

  };

  const aiUrl = `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/ai/run/${model}`;


  const response = await fetch(aiUrl, {

    method: "POST",

    headers: {

      "Content-Type": "application/json",

      Authorization: `Bearer ${env.API_TOKEN}`,

    },

    body: JSON.stringify(requestBody),

  });

  if (!response.ok) {

    console.log(JSON.stringify(await response.text(), null, 2));

    throw new Error(`LLM call failed ${aiUrl} ${response.status}`);

  }


  // process response

  const data = (await response.json()) as { result: { response: string } };

  const text = data.result.response || "";

  const value = (text.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || [

    null,

    text,

  ])[1];

  try {

    return JSON.parse(value);

  } catch (e) {

    console.error(`${e} . Response: ${value}`);

  }

}


```

Explain Code

You can run this script to test it via:

Terminal window

```

npx wrangler dev


```

With your script now running, you can go to `http://localhost:8787/` and should see something like the following:

```

{

  "title": "IP Addresses in 2024",

  "url": "http://example.com/ip-addresses-in-2024",

  "date": "11 Jan 2025"

}


```

For more complex websites or prompts, you might need a better model. Check out the latest models in [Workers AI](https://developers.cloudflare.com/workers-ai/models/).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/how-to/ai/","name":"Use Browser Run with AI"}}]}
```

---

---
title: Deploy a Browser Run Worker with Durable Objects
description: Use the Browser Run API along with Durable Objects to take screenshots from web pages and store them in R2.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ JavaScript ](https://developers.cloudflare.com/search/?tags=JavaScript) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/how-to/browser-run-with-do.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Deploy a Browser Run Worker with Durable Objects

**Last reviewed:**  over 2 years ago 

By following this guide, you will create a Worker that uses the Browser Run API along with [Durable Objects](https://developers.cloudflare.com/durable-objects/) to take screenshots from web pages and store them in [R2](https://developers.cloudflare.com/r2/).

Using Durable Objects to persist browser sessions improves performance by eliminating the time that it takes to spin up a new browser session. Since Durable Objects re-uses sessions, it reduces the number of concurrent sessions needed.

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

## 1\. Create a Worker project

[Cloudflare Workers](https://developers.cloudflare.com/workers/) provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure. Your Worker application is a container to interact with a headless browser to do actions, such as taking screenshots.

Create a new Worker project named `browser-worker` by running:

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- browser-worker
```

```
yarn create cloudflare browser-worker
```

```
pnpm create cloudflare@latest browser-worker
```

## 2\. Install Puppeteer

In your `browser-worker` directory, install Cloudflare’s [fork of Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/):

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/puppeteer
```

```
yarn add -D @cloudflare/puppeteer
```

```
pnpm add -D @cloudflare/puppeteer
```

```
bun add -d @cloudflare/puppeteer
```

## 3\. Create a R2 bucket

Create two R2 buckets, one for production, and one for development.

Note that bucket names must be lowercase and can only contain dashes.

Terminal window

```

wrangler r2 bucket create screenshots

wrangler r2 bucket create screenshots-test


```

To check that your buckets were created, run:

Terminal window

```

wrangler r2 bucket list


```

After running the `list` command, you will see all bucket names, including the ones you have just created.

## 4\. Configure your Wrangler configuration file

Configure your `browser-worker` project's [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) by adding a browser [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) and a [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Browser bindings allow for communication between a Worker and a headless browser which allows you to do actions such as taking a screenshot, generating a PDF and more.

Update your Wrangler configuration file with the Browser Run API binding, the R2 bucket you created and a Durable Object:

Note

Your Worker configuration must include the `nodejs_compat` compatibility flag and a `compatibility_date` of 2025-09-15 or later.

* [  wrangler.jsonc ](#tab-panel-3560)
* [  wrangler.toml ](#tab-panel-3561)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "rendering-api-demo",

  "main": "src/index.js",

  // Set this to today's date

  "compatibility_date": "2026-04-16",

  "compatibility_flags": ["nodejs_compat"],

  "account_id": "<ACCOUNT_ID>",

  // Browser Run API binding

  "browser": {

    "binding": "MYBROWSER",

  },

  // Bind an R2 Bucket

  "r2_buckets": [

    {

      "binding": "BUCKET",

      "bucket_name": "screenshots",

      "preview_bucket_name": "screenshots-test",

    },

  ],

  // Binding to a Durable Object

  "durable_objects": {

    "bindings": [

      {

        "name": "BROWSER",

        "class_name": "Browser",

      },

    ],

  },

  "migrations": [

    {

      "tag": "v1", // Should be unique for each entry

      "new_sqlite_classes": [

        // Array of new classes

        "Browser",

      ],

    },

  ],

}


```

Explain Code

TOML

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "rendering-api-demo"

main = "src/index.js"

# Set this to today's date

compatibility_date = "2026-04-16"

compatibility_flags = [ "nodejs_compat" ]

account_id = "<ACCOUNT_ID>"


[browser]

binding = "MYBROWSER"


[[r2_buckets]]

binding = "BUCKET"

bucket_name = "screenshots"

preview_bucket_name = "screenshots-test"


[[durable_objects.bindings]]

name = "BROWSER"

class_name = "Browser"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "Browser" ]


```

Explain Code

## 5\. Code

The code below uses Durable Object to instantiate a browser using Puppeteer. It then opens a series of web pages with different resolutions, takes a screenshot of each, and uploads it to R2.

The Durable Object keeps a browser session open for 60 seconds after last use. If a browser session is open, any requests will re-use the existing session rather than creating a new one. Update your Worker code by copy and pasting the following:

* [  JavaScript ](#tab-panel-3562)
* [  TypeScript ](#tab-panel-3563)

JavaScript

```

import { DurableObject } from "cloudflare:workers";

import * as puppeteer from "@cloudflare/puppeteer";


export default {

  async fetch(request, env) {

    const obj = env.BROWSER.getByName("browser");


    // Send a request to the Durable Object, then await its response

    const resp = await obj.fetch(request);


    return resp;

  },

};


const KEEP_BROWSER_ALIVE_IN_SECONDS = 60;


export class Browser extends DurableObject {

  browser;

  keptAliveInSeconds = 0;

  storage;


  constructor(state, env) {

    super(state, env);

    this.storage = state.storage;

  }


  async fetch(request) {

    // Screen resolutions to test out

    const width = [1920, 1366, 1536, 360, 414];

    const height = [1080, 768, 864, 640, 896];


    // Use the current date and time to create a folder structure for R2

    const nowDate = new Date();

    const coeff = 1000 * 60 * 5;

    const roundedDate = new Date(

      Math.round(nowDate.getTime() / coeff) * coeff,

    ).toString();

    const folder = roundedDate.split(" GMT")[0];


    // If there is a browser session open, re-use it

    if (!this.browser || !this.browser.isConnected()) {

      console.log(`Browser DO: Starting new instance`);

      try {

        this.browser = await puppeteer.launch(this.env.MYBROWSER);

      } catch (e) {

        console.log(

          `Browser DO: Could not start browser instance. Error: ${e}`,

        );

      }

    }


    // Reset keptAlive after each call to the DO

    this.keptAliveInSeconds = 0;


    // Check if browser exists before opening page

    if (!this.browser)

      return new Response("Browser launch failed", { status: 500 });


    const page = await this.browser.newPage();


    // Take screenshots of each screen size

    for (let i = 0; i < width.length; i++) {

      await page.setViewport({ width: width[i], height: height[i] });

      await page.goto("https://workers.cloudflare.com/");

      const fileName = `screenshot_${width[i]}x${height[i]}`;

      const sc = await page.screenshot();


      await this.env.BUCKET.put(`${folder}/${fileName}.jpg`, sc);

    }


    // Close tab when there is no more work to be done on the page

    await page.close();


    // Reset keptAlive after performing tasks to the DO

    this.keptAliveInSeconds = 0;


    // Set the first alarm to keep DO alive

    const currentAlarm = await this.storage.getAlarm();

    if (currentAlarm == null) {

      console.log(`Browser DO: setting alarm`);

      const TEN_SECONDS = 10 * 1000;

      await this.storage.setAlarm(Date.now() + TEN_SECONDS);

    }


    return new Response("success");

  }


  async alarm() {

    this.keptAliveInSeconds += 10;


    // Extend browser DO life

    if (this.keptAliveInSeconds < KEEP_BROWSER_ALIVE_IN_SECONDS) {

      console.log(

        `Browser DO: has been kept alive for ${this.keptAliveInSeconds} seconds. Extending lifespan.`,

      );

      await this.storage.setAlarm(Date.now() + 10 * 1000);

      // You can ensure the ws connection is kept alive by requesting something

      // or just let it close automatically when there is no work to be done

      // for example, `await this.browser.version()`

    } else {

      console.log(

        `Browser DO: exceeded life of ${KEEP_BROWSER_ALIVE_IN_SECONDS}s.`,

      );

      if (this.browser) {

        console.log(`Closing browser.`);

        await this.browser.close();

      }

    }

  }

}


```

[Run Worker in Playground](https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBWQQGZBAFgCMk4ZIBsAJgBcLFm2Ac4XGnwEjxU2fOUBYAFABhdFQgBTO9gAiUAM4x0bqNFsqSmngExCRUcMD2DABEUDT2AB4AdABWblGkqFBgjuGRMXFJqVGWNnaOENgAKnQw9v5wMDBgfARQtsjJcABucG68CLAQANTA6Ljg9paWUMCeSCQA3iTOIAhwZNkA8mTJ9rwQJAC+AQjowCRRvGCUuKhgiHUA7pgA1vYIaUQWM3MHAFQkXokGAgRr2BzvE5nC4AASuNzuD2QILBEIQ6QsFgSvxIuHsqDg4AOCwsJEBbjoVF4AXBvAAFgAKBD2ACOIHsbggABoSI4ugBKRakskkXi2TkkdA7EgMXlULqJABCACVNgB1ADKAFFlYkAObgxV0AByuQZUTIp0ebneUX5X2FZOQyBIGscuEBJGZbI5Bwg6BIEDp9mWq3W2RI212+x5QccgMecB8JB8bi9HM8VBtjtF4oOzI8MoTSYOUuSiVQtMZ3vZnPtmJF6Ygqyo6Y8XzJhy5FkODrFWYOAGktVqAAoAfRV6u1yvHAEEADIASQAalrx0vjePtVZNsbnBqiwpBA7sZgDldemnFVabQhefEHDQ0ys1ht7FG9sThZb0Nb3h2JBvDAEBzs0XT2EuVBuv2uBprKJ7CpymBwAaDpkv2nIICA+yYAynIEPYPJ8oKJKNm4oLvPhhAOMR8r1o2QbuIkyFrAaRYEQ4LH+mx9iAYcDbkpS1KVhA9JMqytYQKRObOq6-T2PGBboGAIC+FmgYBg4EqUBAOaYQcjxQLgQZFgA2tIACcSiCDy0hiAoCh2cIDk8g5tkkDIkgALqARheYkMGUB6nSByyhZggABweQA7AokU8pFCiSDyyUeZFlkKL5glOi6ACqNqBsGoqrMydi4oRgI0IGMwhv6orMpVcABCpeL3lhOHNsyLX3sqSj6QFVB-s4lWylQ9iPMshEMgxIoGbm+KoEW0iCKtJAAse60kMIfm5gOXqUHEuAjQ4RbjZNJ32AyOZkgAsgQdKJKc3AMkNjyXfq4KVLVM0kC6YqLYKAIA6gqDdo2-KJP6GoQAMVB6jNu3zRgYBtUWz1HR9HjNBA5okAA4rdlR2mZgjZbJLpLktcbde4nq-v+7Uct4tiSrUVA8syXCFT4OZQEtDIAIRMW4iQM3eJAAD6SyQwt0sx4vvIk7ilON+z2LgM0yY2-lZip9iJNcCMAAY3n+EvOJs-gw4g0Dw6EE0pgOcBUvYxuzYx9BCjrIoi2Lt6QrKcCJsmKK1GihuElSjJ+3yiS3QAmlOmo6h7IrHLwBD0iQDL2NrPt7W4+uG+gCM3TrpsB-elv+DY4AekNBwEfMiv3rEBGu4kJBaggpwIP4AAkCz2IcxvgwXaedjmAkUyQyocuCQH2CBYFQBBgKoA4972HA2eZ2AYCaUVIaWzmfvAaB4GQdBey2HBRaIbPVjBrwLwpktrcPu4EBpmQ+KYCGdA7NYh6mBKhewfMBZywVlXfk5dmRdVbOdOeGZxRXSiGbRmJB7jUGzgSLIGsog8iWJxEAbh-CiEEEcesA19rwHYkHEOfp5ai1bokc6o5wGIxyn9F0lQ4BvBIH0Zkjg3B0nQD-SUS0d7Z2EYpVs3gABeEDGxsBztkA4UAH6kC0QAHhIEZEyj1sjwyDDooYQx86NmDiWMBBoWLghXFACavwGRLEMUGfwHi6RmSgN5HkQUQoQH8IE0KvjvLUN2mSGxodwH6gkegc0oUIAwHIc6Z4CA3gfESPCEAtx7jMhyWcZAdoomFwOJkbIpoIhFmNnI0R4iIDjiHt48Jhx4hD1CRANpxsynzT6EWGJBx6EG3qVmRp3Dy5DKKsxOOio8pWGHJURIIJcbGyHijNqhxkAbIIdUkeKQYB6jHkI3gacZ6NjklYa4hVCBkAMcGVsNMQx0yGiQUY3UMlv3qn-XEthAFPOKiMnM0yRk5JuVdGhlyXTzxtAcC+q915wE3pCWoCBNAgMDL0F4aZ6pxmWJsM+LDEgIqvlBGCd94IkEftC10i98WZA+AcOABTzj1TeMvAlgIr60IlLwUq5QwKIHOIw2xftWJxINJfYV3DGz8xzvy3ugrWUyjGuAMAVi5rimLkbBkldzaQhrkI8EdtQEsuFe7PpAVKhai3DuPcB5lpUIBCtVaZTpnip4nEuFQqsAMg+m9X6QwSA2rtVqXc+4NTnJ4QglsDtJqwszDac0FFeC8GZqU4UFyhJUm5TKzV59l6XzXtfClz4SBDFlCtdCIo5JakfO6cgVcuXNErJAnOhaV5kpvrBNM+jhxjknKqFOs5FyrnXJubc4aHVRu9jrTCOrS7XQLiQfVWCjV0iBH-eMF9uUlp6iQIenbi0QXJbfZ8xwbS9q7vWp8PB7atozC7RIY9y6T2LMmT1KF7E+tZf6wi7C-xBpICtLarrBDvrkgnSgooXZygot1fF1o9pq3UimNMu6WX7rIHQdMPpOSYqLhEJi8Ny5yUwCQZIZCDgaJTBeCFgI1JnFaPvMAuHHiPOPrTNMbyvlH1+bgf5ZGXRqISOEJoRFV0euJWwiCHw2hUBmsbaevIwCFTIvO7V2QS5lxXWui2VsHzpo1hrbB-NAFLSHgOicycZzzmXGuDcYaI0HkOKLV9Pt33yoZH7VumrGwLu07q421yvCYrYZa8u0SmEzNYVXcFXhIVlIEo2FLRweyWHUMwTQ2hdA8H4EIUQEgvKmCUCUWwT4KiuA8GF9S-hAjaFIGECI0RiNwG0OkAIBDmt5A2FKYo1gKvlCqDUOogJGjNEzupDoRcqBTAsAsKIwAkxUHHKMcY2QogqHyHiQoaRDiZayzl4IeX9CFaMCVxQShmCWCAA)

TypeScript

```

import { DurableObject } from "cloudflare:workers";

import * as puppeteer from "@cloudflare/puppeteer";


interface Env {

  MYBROWSER: Fetcher;

  BUCKET: R2Bucket;

  BROWSER: DurableObjectNamespace;

}


export default {

  async fetch(request, env): Promise<Response> {

    const obj = env.BROWSER.getByName("browser");


    // Send a request to the Durable Object, then await its response

    const resp = await obj.fetch(request);


    return resp;

  },

} satisfies ExportedHandler<Env>;


const KEEP_BROWSER_ALIVE_IN_SECONDS = 60;


export class Browser extends DurableObject<Env> {

  private browser?: puppeteer.Browser;

  private keptAliveInSeconds: number = 0;

  private storage: DurableObjectStorage;


  constructor(state: DurableObjectState, env: Env) {

    super(state, env);

    this.storage = state.storage;

  }


  async fetch(request: Request): Promise<Response> {

    // Screen resolutions to test out

    const width: number[] = [1920, 1366, 1536, 360, 414];

    const height: number[] = [1080, 768, 864, 640, 896];


    // Use the current date and time to create a folder structure for R2

    const nowDate = new Date();

    const coeff = 1000 * 60 * 5;

    const roundedDate = new Date(

      Math.round(nowDate.getTime() / coeff) * coeff,

    ).toString();

    const folder = roundedDate.split(" GMT")[0];


    // If there is a browser session open, re-use it

    if (!this.browser || !this.browser.isConnected()) {

      console.log(`Browser DO: Starting new instance`);

      try {

        this.browser = await puppeteer.launch(this.env.MYBROWSER);

      } catch (e) {

        console.log(

          `Browser DO: Could not start browser instance. Error: ${e}`,

        );

      }

    }


    // Reset keptAlive after each call to the DO

    this.keptAliveInSeconds = 0;


    // Check if browser exists before opening page

    if (!this.browser)

      return new Response("Browser launch failed", { status: 500 });


    const page = await this.browser.newPage();


    // Take screenshots of each screen size

    for (let i = 0; i < width.length; i++) {

      await page.setViewport({ width: width[i], height: height[i] });

      await page.goto("https://workers.cloudflare.com/");

      const fileName = `screenshot_${width[i]}x${height[i]}`;

      const sc = await page.screenshot();


      await this.env.BUCKET.put(`${folder}/${fileName}.jpg`, sc);

    }


    // Close tab when there is no more work to be done on the page

    await page.close();


    // Reset keptAlive after performing tasks to the DO

    this.keptAliveInSeconds = 0;


    // Set the first alarm to keep DO alive

    const currentAlarm = await this.storage.getAlarm();

    if (currentAlarm == null) {

      console.log(`Browser DO: setting alarm`);

      const TEN_SECONDS = 10 * 1000;

      await this.storage.setAlarm(Date.now() + TEN_SECONDS);

    }


    return new Response("success");

  }


  async alarm(): Promise<void> {

    this.keptAliveInSeconds += 10;


    // Extend browser DO life

    if (this.keptAliveInSeconds < KEEP_BROWSER_ALIVE_IN_SECONDS) {

      console.log(

        `Browser DO: has been kept alive for ${this.keptAliveInSeconds} seconds. Extending lifespan.`,

      );

      await this.storage.setAlarm(Date.now() + 10 * 1000);

      // You can ensure the ws connection is kept alive by requesting something

      // or just let it close automatically when there is no work to be done

      // for example, `await this.browser.version()`

    } else {

      console.log(

        `Browser DO: exceeded life of ${KEEP_BROWSER_ALIVE_IN_SECONDS}s.`,

      );

      if (this.browser) {

        console.log(`Closing browser.`);

        await this.browser.close();

      }

    }

  }

}


```

Explain Code

## 6\. Test

Run `npx wrangler dev` to test your Worker locally.

Use real headless browser during local development

To interact with a real headless browser during local development, set `"remote" : true` in the Browser binding configuration. Learn more in our [remote bindings documentation](https://developers.cloudflare.com/workers/development-testing/#remote-bindings).

## 7\. Deploy

Run [npx wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#deploy) to deploy your Worker to the Cloudflare global network.

## Related resources

* Other [Puppeteer examples ↗](https://github.com/cloudflare/puppeteer/tree/main/examples)
* Get started with [Durable Objects](https://developers.cloudflare.com/durable-objects/get-started/)
* [Using R2 from Workers](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/how-to/browser-run-with-do/","name":"Deploy a Browser Run Worker with Durable Objects"}}]}
```

---

---
title: Deploy a Browser Run Worker
description: By following this guide, you will create a Worker that uses the Browser Run API to take screenshots from web pages. This is a common use case for browser automation.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/how-to/deploy-worker.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Deploy a Browser Run Worker

By following this guide, you will create a Worker that uses the Browser Run API to take screenshots from web pages. This is a common use case for browser automation.

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

#### 1\. Create a Worker project

[Cloudflare Workers](https://developers.cloudflare.com/workers/) provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure. Your Worker application is a container to interact with a headless browser to do actions, such as taking screenshots.

Create a new Worker project named `browser-worker` by running:

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- browser-worker
```

```
yarn create cloudflare browser-worker
```

```
pnpm create cloudflare@latest browser-worker
```

For setup, select the following options:

* For _What would you like to start with?_, choose `Hello World example`.
* For _Which template would you like to use?_, choose `Worker only`.
* For _Which language do you want to use?_, choose `JavaScript / TypeScript`.
* For _Do you want to use git for version control?_, choose `Yes`.
* For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying).

#### 2\. Install Puppeteer

In your `browser-worker` directory, install Cloudflare’s [fork of Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/):

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/puppeteer
```

```
yarn add -D @cloudflare/puppeteer
```

```
pnpm add -D @cloudflare/puppeteer
```

```
bun add -d @cloudflare/puppeteer
```

#### 3\. Create a KV namespace

Browser Run can be used with other developer products. You might need a [relational database](https://developers.cloudflare.com/d1/), an [R2 bucket](https://developers.cloudflare.com/r2/) to archive your crawled pages and assets, a [Durable Object](https://developers.cloudflare.com/durable-objects/) to keep your browser instance alive and share it with multiple requests, or [Queues](https://developers.cloudflare.com/queues/) to handle your jobs asynchronously.

For the purpose of this example, we will use a [KV store](https://developers.cloudflare.com/kv/concepts/kv-namespaces/) to cache your screenshots.

Create two namespaces, one for production and one for development.

Terminal window

```

npx wrangler kv namespace create BROWSER_KV_DEMO

npx wrangler kv namespace create BROWSER_KV_DEMO --preview


```

Take note of the IDs for the next step.

#### 4\. Configure the Wrangler configuration file

Configure your `browser-worker` project's [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) by adding a browser [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) and a [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Bindings allow your Workers to interact with resources on the Cloudflare developer platform. Your browser `binding` name is set by you, this guide uses the name `MYBROWSER`. Browser bindings allow for communication between a Worker and a headless browser which allows you to do actions such as taking a screenshot, generating a PDF, and more.

Update your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) with the Browser Run API binding and the KV namespaces you created:

* [  wrangler.jsonc ](#tab-panel-3566)
* [  wrangler.toml ](#tab-panel-3567)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "browser-worker",

  "main": "src/index.js",

  // Set this to today's date

  "compatibility_date": "2026-04-16",

  "compatibility_flags": ["nodejs_compat"],

  "browser": {

    "binding": "MYBROWSER"

  },

  "kv_namespaces": [

    {

      "binding": "BROWSER_KV_DEMO",

      "id": "22cf855786094a88a6906f8edac425cd",

      "preview_id": "e1f8b68b68d24381b57071445f96e623"

    }

  ]

}


```

Explain Code

TOML

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "browser-worker"

main = "src/index.js"

# Set this to today's date

compatibility_date = "2026-04-16"

compatibility_flags = [ "nodejs_compat" ]


[browser]

binding = "MYBROWSER"


[[kv_namespaces]]

binding = "BROWSER_KV_DEMO"

id = "22cf855786094a88a6906f8edac425cd"

preview_id = "e1f8b68b68d24381b57071445f96e623"


```

Explain Code

#### 5\. Code

* [  JavaScript ](#tab-panel-3564)
* [  TypeScript ](#tab-panel-3565)

Update `src/index.js` with your Worker code:

JavaScript

```

import puppeteer from "@cloudflare/puppeteer";


export default {

  async fetch(request, env) {

    const { searchParams } = new URL(request.url);

    let url = searchParams.get("url");

    let img;

    if (url) {

      url = new URL(url).toString(); // normalize

      img = await env.BROWSER_KV_DEMO.get(url, { type: "arrayBuffer" });

      if (img === null) {

        const browser = await puppeteer.launch(env.MYBROWSER);

        const page = await browser.newPage();

        await page.goto(url);

        img = await page.screenshot();

        await env.BROWSER_KV_DEMO.put(url, img, {

          expirationTtl: 60 * 60 * 24,

        });

        await browser.close();

      }

      return new Response(img, {

        headers: {

          "content-type": "image/jpeg",

        },

      });

    } else {

      return new Response("Please add an ?url=https://example.com/ parameter");

    }

  },

};


```

Explain Code

Update `src/index.ts` with your Worker code:

TypeScript

```

import puppeteer from "@cloudflare/puppeteer";


interface Env {

  MYBROWSER: Fetcher;

  BROWSER_KV_DEMO: KVNamespace;

}


export default {

  async fetch(request, env): Promise<Response> {

    const { searchParams } = new URL(request.url);

    let url = searchParams.get("url");

    let img: Buffer;

    if (url) {

      url = new URL(url).toString(); // normalize

      img = await env.BROWSER_KV_DEMO.get(url, { type: "arrayBuffer" });

      if (img === null) {

        const browser = await puppeteer.launch(env.MYBROWSER);

        const page = await browser.newPage();

        await page.goto(url);

        img = (await page.screenshot()) as Buffer;

        await env.BROWSER_KV_DEMO.put(url, img, {

          expirationTtl: 60 * 60 * 24,

        });

        await browser.close();

      }

      return new Response(img, {

        headers: {

          "content-type": "image/jpeg",

        },

      });

    } else {

      return new Response("Please add an ?url=https://example.com/ parameter");

    }

  },

} satisfies ExportedHandler<Env>;


```

Explain Code

This Worker instantiates a browser using Puppeteer, opens a new page, navigates to the location of the 'url' parameter, takes a screenshot of the page, stores the screenshot in KV, closes the browser, and responds with the JPEG image of the screenshot.

If your Worker is running in production, it will store the screenshot to the production KV namespace. If you are running `wrangler dev`, it will store the screenshot to the dev KV namespace.

If the same `url` is requested again, it will use the cached version in KV instead, unless it expired.

#### 6\. Test

Run `npx wrangler dev` to test your Worker locally.

Use real headless browser during local development

To interact with a real headless browser during local development, set `"remote" : true` in the Browser binding configuration. Learn more in our [remote bindings documentation](https://developers.cloudflare.com/workers/development-testing/#remote-bindings).

To test taking your first screenshot, go to the following URL:

`<LOCAL_HOST_URL>/?url=https://example.com`

#### 7\. Deploy

Run `npx wrangler deploy` to deploy your Worker to the Cloudflare global network.

To take your first screenshot, go to the following URL:

```

<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/?url=https://example.com


```

## Related resources

* Other [Puppeteer examples ↗](https://github.com/cloudflare/puppeteer/tree/main/examples)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/how-to/deploy-worker/","name":"Deploy a Browser Run Worker"}}]}
```

---

---
title: Generate OG images for Astro sites
description: Open Graph (OG) images are the preview images that appear when you share a link on social media. Instead of manually creating these images for every blog post, you can use Cloudflare Browser Run to automatically generate branded social preview images from an Astro template.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/how-to/og-images-astro.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Generate OG images for Astro sites

Open Graph (OG) images are the preview images that appear when you share a link on social media. Instead of manually creating these images for every blog post, you can use Cloudflare Browser Run to automatically generate branded social preview images from an Astro template.

In this tutorial, you will:

1. Create an Astro page that renders your OG image design.
2. Use Browser Run to screenshot that page as a PNG.
3. Serve the generated images to social media crawlers.

## Prerequisites

* A Cloudflare account with [Browser Run enabled](https://developers.cloudflare.com/browser-run/get-started/#quick-actions)
* An Astro site deployed on [Cloudflare Workers](https://developers.cloudflare.com/workers/framework-guides/web-apps/astro/)
* Basic familiarity with Astro and Cloudflare Workers

## 1\. Create the OG image template

Create an Astro route that renders your OG image design. This page serves as the source of truth for your image layout.

Create `src/pages/social-card.astro`:

```

---

export const prerender = false;


const title = Astro.url.searchParams.get("title") || "Untitled";

const image = Astro.url.searchParams.get("image");

const author = Astro.url.searchParams.get("author");

---


<html>

  <head>

    <meta charset="utf-8" />

    <style>

      * {

        margin: 0;

        padding: 0;

        box-sizing: border-box;

      }

      body {

        width: 1200px;

        height: 630px;

        display: flex;

        flex-direction: column;

        justify-content: flex-end;

        padding: 60px;

        font-family: system-ui, sans-serif;

        background: linear-gradient(135deg, #f38020 0%, #f9a825 100%);

        color: white;

      }

      .title {

        font-size: 64px;

        font-weight: bold;

        line-height: 1.1;

        margin-bottom: 24px;

      }

      .author {

        font-size: 24px;

        opacity: 0.9;

      }

      .logo {

        position: absolute;

        top: 60px;

        left: 60px;

        height: 40px;

      }

    </style>

  </head>

  <body>

    <img class="logo" src="/your-logo.png" alt="Your logo" />

    <h1 class="title">{title}</h1>

    {author && <p class="author">By {author}</p>}

  </body>

</html>


```

Explain Code

Start your Astro development server to test the template:

Terminal window

```

npm run dev


```

Test locally by visiting `http://localhost:4321/social-card?title=My%20Blog%20Post&author=Omar`.

Note

This tutorial assumes your markdown posts have frontmatter fields for `title`, `slug`, and optionally `author`. For example:

YAML

```

---

title: "My First Post"

slug: "my-first-post"

author: "John Doe"

---


```

Adjust the `readPosts()` function in the script to match your frontmatter structure.

Before proceeding, deploy your site to ensure the `/social-card` route is live:

Terminal window

```

# For Cloudflare Workers

npx wrangler deploy


```

Update the `BASE_URL` in the script below to match your deployed site URL.

## 2\. Generate OG images at build time

Generate all OG images during the Astro build process using Cloudflare Browser Run Quick Actions.

Create `scripts/generate-social-cards.ts`:

TypeScript

```

import {

  existsSync,

  mkdirSync,

  readdirSync,

  readFileSync,

  writeFileSync,

} from "fs";

import { join } from "path";


// Configuration

const BASE_URL = "https://your-site.com"; // Your deployed site URL

const CF_API = "https://api.cloudflare.com/client/v4/accounts";

const OUTPUT_DIR = "public/social-cards"; // Output directory for generated images

const POSTS_DIR = "src/data/posts"; // Directory containing your markdown posts (adjust to match your project)


interface Post {

  slug: string;

  title: string;

  author?: string;

}


/** Extract a frontmatter field value from raw markdown content. */

function getFrontmatterField(content: string, field: string): string | null {

  const match = content.match(new RegExp(`^${field}:\\s*"?([^"\\n]+)"?`, "m"));

  return match ? match[1].trim() : null;

}


/**

 * Read all post files and return { slug, title, author }[].

 * This function scans the POSTS_DIR for markdown files, extracts frontmatter

 * fields (slug, title, author), and returns an array of post objects.

 * Falls back to filename for slug and slug for title if frontmatter is missing.

 */

function readPosts(): Post[] {

  if (!existsSync(POSTS_DIR)) return [];

  const files = readdirSync(POSTS_DIR).filter((f) => f.endsWith(".md"));

  return files.map((file) => {

    const raw = readFileSync(join(POSTS_DIR, file), "utf-8");

    const slug = getFrontmatterField(raw, "slug") ?? file.replace(/\.md$/, "");

    const title = getFrontmatterField(raw, "title") ?? slug;

    const author = getFrontmatterField(raw, "author") ?? undefined;

    return { slug, title, author };

  });

}


/**

 * Capture a screenshot using Cloudflare Browser Run Quick Actions

 */

async function captureScreenshot(

  accountId: string,

  apiToken: string,

  pageUrl: string,

): Promise<ArrayBuffer> {

  const endpoint = `${CF_API}/${accountId}/browser-rendering/screenshot`;


  const res = await fetch(endpoint, {

    method: "POST",

    headers: {

      Authorization: `Bearer ${apiToken}`,

      "Content-Type": "application/json",

    },

    body: JSON.stringify({

      url: pageUrl,

      viewport: { width: 1200, height: 630 }, // Standard OG image size

      gotoOptions: { waitUntil: "networkidle0" }, // Wait for page to fully load

    }),

  });


  if (!res.ok) {

    const text = await res.text();

    throw new Error(`Screenshot API returned ${res.status}: ${text}`);

  }


  return res.arrayBuffer();

}


async function main() {

  // Read credentials from environment variables

  const accountId = process.env.CF_ACCOUNT_ID;

  const apiToken = process.env.CF_API_TOKEN;


  if (!accountId || !apiToken) {

    console.error("Error: CF_ACCOUNT_ID and CF_API_TOKEN required");

    process.exit(1);

  }


  // Check if --force flag is passed to regenerate all images

  const force = process.argv.includes("--force");


  // Read posts from markdown files

  const posts = readPosts();


  if (posts.length === 0) {

    console.log("No posts found. Check your POSTS_DIR path.");

    process.exit(0);

  }


  console.log(`Found ${posts.length} posts to process\n`);


  // Ensure output directory exists

  mkdirSync(OUTPUT_DIR, { recursive: true });


  let generated = 0;

  let skipped = 0;


  // Generate social card for each post

  for (let i = 0; i < posts.length; i++) {

    const post = posts[i];

    const outPath = join(OUTPUT_DIR, `${post.slug}.png`);

    const label = `[${i + 1}/${posts.length}]`;


    // Skip if file exists and --force flag not set

    if (!force && existsSync(outPath)) {

      console.log(`${label} ${post.slug}.png — skipped (exists)`);

      skipped++;

      continue;

    }


    // Build URL with query parameters for the OG template

    const params = new URLSearchParams({

      title: post.title,

      author: post.author || "",

    });

    const url = `${BASE_URL}/social-card?${params}`;


    try {

      // Capture screenshot and save to file

      const png = await captureScreenshot(accountId, apiToken, url);

      writeFileSync(outPath, Buffer.from(png));

      console.log(`${label} ${post.slug}.png — done`);

      generated++;

    } catch (err) {

      console.error(`${label} ${post.slug}.png — failed:`, err);

    }


    // Rate limiting: small delay between requests

    if (i < posts.length - 1) {

      await new Promise((resolve) => setTimeout(resolve, 200));

    }

  }


  console.log(`\nDone. Generated: ${generated}, Skipped: ${skipped}`);

}


main();


```

Explain Code

Set your Cloudflare credentials as environment variables:

Terminal window

```

export CF_ACCOUNT_ID=your_account_id

export CF_API_TOKEN=your_api_token


```

Note

Browser Run has [rate limits](https://developers.cloudflare.com/browser-run/limits/) that vary by plan. The script includes a 200ms delay between requests to help stay within these limits. For large sites, you may need to run the script in batches.

Run the script to generate images:

Terminal window

```

# Generate new images only

bun scripts/generate-social-cards.ts


# Regenerate all images

bun scripts/generate-social-cards.ts --force


```

Optionally, add to your build script in `package.json`:

```

{

  "scripts": {

    "build": "bun scripts/generate-social-cards.ts && astro build"

  }

}


```

## 3\. Add OG meta tags to your pages

Update your blog post layout to reference the generated images:

```

---

// src/layouts/BlogPost.astro

const { title, slug, author } = Astro.props;

const ogImageUrl = `/social-cards/${slug}.png`;

---


<html>

  <head>

    <meta property="og:title" content={title} />

    <meta property="og:image" content={ogImageUrl} />

    <meta property="og:image:width" content="1200" />

    <meta property="og:image:height" content="630" />

    <meta name="twitter:card" content="summary_large_image" />

    <meta name="twitter:image" content={ogImageUrl} />

  </head>

  <body>

    <slot />

  </body>

</html>


```

Explain Code

## 4\. Test your OG images

Before testing, make sure to deploy your site with the newly generated social card images:

Terminal window

```

# For Cloudflare Workers

npx wrangler deploy


```

Use these tools to verify your OG images render correctly:

* [Facebook Sharing Debugger ↗](https://developers.facebook.com/tools/debug/)
* [Twitter Card Validator ↗](https://cards-dev.twitter.com/validator)
* [LinkedIn Post Inspector ↗](https://www.linkedin.com/post-inspector/)

## Customize the template

### Add a background image

```

---

const title = Astro.url.searchParams.get("title") || "Untitled";

const image = Astro.url.searchParams.get("image");

---


<body style={image ? `background-image: url(${image})` : undefined}>

  <!-- content -->

</body>


```

### Use custom fonts

```

<head>

  <link

    href="https://fonts.googleapis.com/css2?family=Inter:wght@700&display=swap"

    rel="stylesheet"

  />

  <style>

    body {

      font-family: "Inter", sans-serif;

    }

  </style>

</head>


```

Explain Code

### Add Tailwind CSS

If your Astro site uses Tailwind, you can use it in your OG template:

```

---

import "../styles/global.css";

---


<body

  class="flex h-[630px] w-[1200px] flex-col justify-end bg-gradient-to-br from-orange-500 to-amber-500 p-16 text-white"

>

  <h1 class="mb-6 text-6xl leading-tight font-bold">{title}</h1>

</body>


```

## Performance considerations

### Image optimization

Consider running generated images through Cloudflare Images or Image Resizing for additional optimization:

TypeScript

```

const optimizedUrl = `https://your-domain.com/cdn-cgi/image/width=1200,format=auto/social-cards/${slug}.png`;


```

## Next steps

Your Astro site now automatically generates OG images using Browser Run. When you share a link on social media, crawlers will fetch the generated image from the static path.

From here, you can:

* Customize your template with [custom fonts](#use-custom-fonts), [Tailwind CSS](#add-tailwind-css), or [background images](#add-a-background-image).
* Add cache invalidation logic to regenerate images when post content changes.
* Use [Cloudflare Images](https://developers.cloudflare.com/images/) or [Image Resizing](https://developers.cloudflare.com/images/transform-images/) for additional optimization.

## Related resources

* [Browser Run documentation](https://developers.cloudflare.com/browser-run/)
* [R2 storage](https://developers.cloudflare.com/r2/)
* [Cloudflare Images](https://developers.cloudflare.com/images/)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/how-to/og-images-astro/","name":"Generate OG images for Astro sites"}}]}
```

---

---
title: Generate PDFs Using HTML and CSS
description: As seen in the Deploy a Browser Run Worker guide, Browser Run can be used to generate screenshots for any given URL. Alongside screenshots, you can also generate full PDF documents for a given webpage, and can also provide the webpage markup and style ourselves.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/how-to/pdf-generation.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Generate PDFs Using HTML and CSS

As seen in the [Deploy a Browser Run Worker](https://developers.cloudflare.com/browser-run/how-to/deploy-worker/) guide, Browser Run can be used to generate screenshots for any given URL. Alongside screenshots, you can also generate full PDF documents for a given webpage, and can also provide the webpage markup and style ourselves.

You can generate PDFs with Browser Run in two ways:

* **[Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/)**: Use the [/pdf endpoint](https://developers.cloudflare.com/browser-run/quick-actions/pdf-endpoint/). This is ideal if you do not need to customize rendering behavior.
* **[Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) or [Playwright](https://developers.cloudflare.com/browser-run/playwright/)**: Use browser automation within Workers for additional control and customization.

Choose the method that best fits your use case.

The following example shows you how to generate a PDF using [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/).

## Prerequisites

1. Use the `create-cloudflare` CLI to generate a new Hello World Cloudflare Worker script:

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- browser-worker
```

```
yarn create cloudflare browser-worker
```

```
pnpm create cloudflare@latest browser-worker
```

1. Install `@cloudflare/puppeteer`, which allows you to control the Browser Run instance:

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/puppeteer
```

```
yarn add -D @cloudflare/puppeteer
```

```
pnpm add -D @cloudflare/puppeteer
```

```
bun add -d @cloudflare/puppeteer
```

1. Add your Browser Run binding to your new Wrangler configuration:

* [  wrangler.jsonc ](#tab-panel-3568)
* [  wrangler.toml ](#tab-panel-3569)

JSONC

```

{

  "browser": {

    "binding": "BROWSER",

  },

}


```

TOML

```

[browser]

binding = "BROWSER"


```

Use real headless browser during local development

To interact with a real headless browser during local development, set `"remote" : true` in the Browser binding configuration. Learn more in our [remote bindings documentation](https://developers.cloudflare.com/workers/development-testing/#remote-bindings).

1. Replace the contents of `src/index.ts` (or `src/index.js` for JavaScript projects) with the following skeleton script:

TypeScript

```

import puppeteer from "@cloudflare/puppeteer";


const generateDocument = (name: string) => {};


export default {

  async fetch(request, env) {

    const { searchParams } = new URL(request.url);

    let name = searchParams.get("name");


    if (!name) {

      return new Response("Please provide a name using the ?name= parameter");

    }


    const browser = await puppeteer.launch(env.BROWSER);

    const page = await browser.newPage();


    // Step 1: Define HTML and CSS

    const document = generateDocument(name);


    // Step 2: Send HTML and CSS to our browser

    await page.setContent(document);


    // Step 3: Generate and return PDF


    return new Response();

  },

};


```

Explain Code

## 1\. Define HTML and CSS

Rather than using Browser Run to navigate to a user-provided URL, manually generate a webpage, then provide that webpage to the Browser Run instance. This allows you to render any design you want.

Note

You can generate your HTML or CSS using any method you like. This example uses string interpolation, but the method is also fully compatible with web frameworks capable of rendering HTML on Workers such as React, Remix, and Vue.

For this example, we are going to take in user-provided content (via a '?name=' parameter), and have that name output in the final PDF document.

To start, fill out your `generateDocument` function with the following:

TypeScript

```

const generateDocument = (name: string) => {

  return `

<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <style>

      html,

      body,

      #container {

        width: 100%;

        height: 100%;

        margin: 0;

      }

      body {

        font-family: Baskerville, Georgia, Times, serif;

        background-color: #f7f1dc;

      }

      strong {

        color: #5c594f;

        font-size: 128px;

        margin: 32px 0 48px 0;

      }

      em {

        font-size: 24px;

      }

      #container {

        flex-direction: column;

        display: flex;

        align-items: center;

        justify-content: center;

        text-align: center;

      }

    </style>

  </head>


  <body>

    <div id="container">

      <em>This is to certify that</em>

      <strong>${name}</strong>

      <em>has rendered a PDF using Cloudflare Workers</em>

    </div>

  </body>

</html>

`;

};


```

Explain Code

This example HTML document should render a beige background imitating a certificate showing that the user-provided name has successfully rendered a PDF using Cloudflare Workers.

Note

It is usually best to avoid directly interpolating user-provided content into an image or PDF renderer in production applications. To render contents like an invoice, it would be best to validate the data input and fetch the data yourself using tools like [D1](https://developers.cloudflare.com/d1/) or [Workers KV](https://developers.cloudflare.com/kv/).

## 2\. Load HTML and CSS Into Browser

Now that you have your fully styled HTML document, you can take the contents and send it to your browser instance. Create an empty page to store this document as follows:

TypeScript

```

const browser = await puppeteer.launch(env.BROWSER);

const page = await browser.newPage();


```

The [page.setContent() ↗](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.page.setcontent.md) function can then be used to set the page's HTML contents from a string, so you can pass in your created document directly like so:

TypeScript

```

await page.setContent(document);


```

## 3\. Generate and Return PDF

With your Browser Run instance now rendering your provided HTML and CSS, you can use the [page.pdf() ↗](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.page.pdf.md) command to generate a PDF file and return it to the client.

TypeScript

```

let pdf = page.pdf({ printBackground: true });


```

The `page.pdf()` call supports a [number of options ↗](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.pdfoptions.md), including setting the dimensions of the generated PDF to a specific paper size, setting specific margins, and allowing fully-transparent backgrounds. For now, you are only overriding the `printBackground` option to allow your `body` background styles to show up.

Now that you have your PDF data, return it to the client in the `Response` with an `application/pdf` content type:

TypeScript

```

return new Response(pdf, {

  headers: {

    "content-type": "application/pdf",

  },

});


```

## Conclusion

The full Worker script now looks as follows:

TypeScript

```

import puppeteer from "@cloudflare/puppeteer";


const generateDocument = (name: string) => {

  return `

<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <style>

    html, body, #container {

    width: 100%;

      height: 100%;

    margin: 0;

    }

      body {

        font-family: Baskerville, Georgia, Times, serif;

        background-color: #f7f1dc;

      }

      strong {

        color: #5c594f;

    font-size: 128px;

    margin: 32px 0 48px 0;

      }

    em {

    font-size: 24px;

    }

      #container {

    flex-direction: column;

        display: flex;

        align-items: center;

        justify-content: center;

    text-align: center

      }

    </style>

  </head>


  <body>

    <div id="container">

    <em>This is to certify that</em>

    <strong>${name}</strong>

    <em>has rendered a PDF using Cloudflare Workers</em>

  </div>

  </body>

</html>

`;

};


export default {

  async fetch(request, env) {

    const { searchParams } = new URL(request.url);

    let name = searchParams.get("name");


    if (!name) {

      return new Response("Please provide a name using the ?name= parameter");

    }


    const browser = await puppeteer.launch(env.BROWSER);

    const page = await browser.newPage();


    // Step 1: Define HTML and CSS

    const document = generateDocument(name);


    // // Step 2: Send HTML and CSS to our browser

    await page.setContent(document);


    // // Step 3: Generate and return PDF

    const pdf = await page.pdf({ printBackground: true });


    // Close browser since we no longer need it

    await browser.close();


    return new Response(pdf, {

      headers: {

        "content-type": "application/pdf",

      },

    });

  },

};


```

Explain Code

You can run this script to test it via:

 npm  yarn  pnpm 

```
npx wrangler dev
```

```
yarn wrangler dev
```

```
pnpm wrangler dev
```

With your script now running, you can pass in a `?name` parameter to the local URL (such as `http://localhost:8787/?name=Harley`) and should see the following:

![A screenshot of a generated PDF, with the author's name shown in a mock certificate.](https://developers.cloudflare.com/_astro/pdf-generation.Diel53Hp_Z27ymFU.webp) 

---

## Custom fonts

If your PDF requires a specific font that is not pre-installed in the Browser Run environment, you can load custom fonts using `addStyleTag`. This allows you to inject fonts from a CDN or embed them as Base64 strings before generating your PDF.

For detailed instructions and examples, refer to [Use your own custom font](https://developers.cloudflare.com/browser-run/features/custom-fonts/).

---

Dynamically generating PDF documents solves a number of common use-cases, from invoicing customers to archiving documents to creating dynamic certificates (as seen in the simple example here).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/how-to/pdf-generation/","name":"Generate PDFs Using HTML and CSS"}}]}
```

---

---
title: Build a web crawler with Queues and Browser Run
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/how-to/queues.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build a web crawler with Queues and Browser Run

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/how-to/queues/","name":"Build a web crawler with Queues and Browser Run"}}]}
```

---

---
title: Automatic request headers
description: Cloudflare automatically attaches headers to every request made through Browser Run. These headers make it easy for destination servers to identify that these requests came from Cloudflare.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/reference/automatic-request-headers.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Automatic request headers

Cloudflare automatically attaches headers to every request made through Browser Run. These headers make it easy for destination servers to identify that these requests came from Cloudflare.

## User-Agent

The default User-Agent depends on how you access Browser Run:

| Method                                                                                                                                                                                                     | Default User-Agent                                                                                              | Customizable                                |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/)                                                                                                                              | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 | Yes, using the userAgent parameter          |
| [Crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/)                                                                                                              | CloudflareBrowserRenderingCrawler/1.0                                                                           | No                                          |
| [CDP](https://developers.cloudflare.com/browser-run/cdp/) ([Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/)) | The default User-Agent of the underlying Chrome version                                                         | Yes, via Puppeteer/Playwright configuration |

Note

Because the User-Agent is configurable for most methods and the Chrome version may change as Browser Run updates its underlying browser engine, destination servers should use the non-configurable headers below to identify Browser Run requests rather than relying on the User-Agent string.

## Non-configurable headers

Note

The following headers are meant to ensure transparency and cannot be removed or overridden (with `setExtraHTTPHeaders`, for example).

| Header                        | Description                                                                                                                                                                                                                                                             |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| cf-brapi-request-id           | A unique identifier for the Browser Run request when using [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/)                                                                                                                                |
| cf-brapi-devtools             | A unique identifier for the Browser Run request when using [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/) |
| cf-biso-devtools              | A flag indicating the request originated from Cloudflare's rendering infrastructure                                                                                                                                                                                     |
| Signature-agent               | [The location of the bot public keys ↗](https://web-bot-auth.cloudflare-browser-rendering-085.workers.dev), used to sign the request and verify it came from Cloudflare                                                                                                 |
| Signature and Signature-input | A digital signature, used to validate requests, as shown in [this architecture document ↗](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture)                                                                                               |

### About Web Bot Auth

The `Signature` headers use an authentication method called [Web Bot Auth](https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/). Web Bot Auth leverages cryptographic signatures in HTTP messages to verify that a request comes from an automated bot. To verify a request originated from Cloudflare Browser Run, use the keys found on [this directory ↗](https://web-bot-auth.cloudflare-browser-rendering-085.workers.dev/.well-known/http-message-signatures-directory) to verify the `Signature` and `Signature-Input` found in the headers from the incoming request. A successful verification proves that the request originated from Cloudflare Browser Run and has not been tampered with in transit.

### Bot detection

Browser Run uses different bot detection IDs depending on the method. [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) (excluding the [crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/)), [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), and [CDP](https://developers.cloudflare.com/browser-run/cdp/) share one ID, while the crawl endpoint has its own.

| Method                                                                                                                                                                                                                                                                                   | Bot detection ID |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/), [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), [CDP](https://developers.cloudflare.com/browser-run/cdp/) | 119853733        |
| [Crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/)                                                                                                                                                                                            | 128292352        |

If you are attempting to scan your own zone and want Browser Run to access your website freely without your bot protection configuration interfering, you can create a WAF skip rule to [allowlist Browser Run](https://developers.cloudflare.com/browser-run/faq/#can-i-allowlist-browser-run-on-my-own-website).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/reference/automatic-request-headers/","name":"Automatic request headers"}}]}
```

---

---
title: Browser close reasons
description: A browser session may close for a variety of reasons, occasionally due to connection errors or errors in the headless browser instance. As a best practice, wrap puppeteer.connect or puppeteer.launch in a try...catch statement.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/reference/browser-close-reasons.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Browser close reasons

A browser session may close for a variety of reasons, occasionally due to connection errors or errors in the headless browser instance. As a best practice, wrap `puppeteer.connect` or `puppeteer.launch` in a [try...catch ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) statement.

To find the reason that a browser closed:

1. In the Cloudflare dashboard, go to the **Browser Run** page.  
[ Go to **Browser Run** ](https://dash.cloudflare.com/?to=/:account/workers/browser-run)
2. Select the **Runs** tab.

Browser Run sessions are billed based on [usage](https://developers.cloudflare.com/browser-run/pricing/). We do not charge for sessions that error due to underlying Browser Run infrastructure.

| Reasons a session may end                            |
| ---------------------------------------------------- |
| User opens and closes browser normally.              |
| Browser is idle for 60 seconds.                      |
| Chromium instance crashes.                           |
| Error connecting with the client, server, or Worker. |
| Browser session is evicted.                          |

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/reference/browser-close-reasons/","name":"Browser close reasons"}}]}
```

---

---
title: robots.txt and sitemaps
description: This page provides general guidance on configuring robots.txt and sitemaps for websites you plan to access with Browser Run.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/reference/robots-txt.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# robots.txt and sitemaps

This page provides general guidance on configuring `robots.txt` and sitemaps for websites you plan to access with Browser Run.

## Identifying Browser Run requests

Requests can be identified by the [automatic headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/) that Cloudflare attaches:

* [User-Agent](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#user-agent) — Each Browser Run method has a different default User-Agent, which you can use to write targeted `robots.txt` rules
* `cf-brapi-request-id` — Unique identifier for Quick Actions requests
* `Signature-agent` — Pointer to Cloudflare's bot verification keys

To allow or block Browser Run traffic using WAF rules instead of `robots.txt`, use the [bot detection IDs](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#bot-detection) on the automatic request headers page.

## Best practices for robots.txt

A well-configured `robots.txt` helps crawlers understand which parts of your site they can access.

### Reference your sitemap

Include a reference to your sitemap in `robots.txt` so crawlers can discover your URLs:

robots.txt

```

User-agent: *

Allow: /


Sitemap: https://example.com/sitemap.xml


```

You can list multiple sitemaps:

robots.txt

```

User-agent: *

Allow: /


Sitemap: https://example.com/sitemap.xml

Sitemap: https://example.com/blog-sitemap.xml


```

### Set a crawl delay

Use `crawl-delay` to control how frequently crawlers request pages from your server:

robots.txt

```

User-agent: *

Crawl-delay: 2

Allow: /


Sitemap: https://example.com/sitemap.xml


```

The value is in seconds. A `crawl-delay` of 2 means the crawler waits two seconds between requests.

## Blocking crawlers with robots.txt

If you want to prevent Browser Run (or other crawlers) from accessing your site, you can configure your `robots.txt` to restrict access.

### Block all bots from your entire site

To prevent all crawlers from accessing any page on your site:

robots.txt

```

User-agent: *

Disallow: /


```

This is the most restrictive configuration and blocks all compliant bots, not just Browser Run.

### Block only the /crawl endpoint

The [/crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/) identifies itself with the User-Agent `CloudflareBrowserRenderingCrawler/1.0`. To block the `/crawl` endpoint while allowing all other traffic (including other Browser Run [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) endpoints, which use a [different User-Agent](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/#user-agent)):

robots.txt

```

User-agent: CloudflareBrowserRenderingCrawler

Disallow: /


User-agent: *

Allow: /


```

### Block the /crawl endpoint on specific paths

To allow the [/crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/) to access your site but block specific sections:

robots.txt

```

User-agent: CloudflareBrowserRenderingCrawler

Disallow: /admin/

Disallow: /private/

Allow: /


User-agent: *

Allow: /


```

## Best practices for sitemaps

Structure your sitemap to help crawlers process your site efficiently:

sitemap.xml

```

<?xml version="1.0" encoding="UTF-8"?>

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

  <url>

    <loc>https://example.com/important-page</loc>

    <lastmod>2025-01-15T00:00:00+00:00</lastmod>

    <priority>1.0</priority>

  </url>

  <url>

    <loc>https://example.com/other-page</loc>

    <lastmod>2025-01-10T00:00:00+00:00</lastmod>

    <priority>0.5</priority>

  </url>

</urlset>


```

Explain Code

| Attribute  | Purpose                       | Recommendation                                                                           |
| ---------- | ----------------------------- | ---------------------------------------------------------------------------------------- |
| <loc>      | URL of the page               | Required. Use full URLs.                                                                 |
| <lastmod>  | Last modification date        | Include to help the crawler identify updated content. Use ISO 8601 format.               |
| <priority> | Relative importance (0.0-1.0) | Set higher values for important pages. The crawler will process pages in priority order. |

### Sitemap index files

For large sites with multiple sitemaps, use a sitemap index file. Browser Run uses the `depth` parameter to control how many levels of nested sitemaps are crawled:

sitemap.xml

```

<?xml version="1.0" encoding="UTF-8"?>

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

  ...

</urlset>

<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

   <sitemap>

      <loc>https://www.example.com/sitemap-products.xml</loc>

   </sitemap>

   <sitemap>

      <loc>https://www.example.com/sitemap-blog.xml</loc>

   </sitemap>

</sitemapindex>


```

Explain Code

### Caching headers

Browser Run periodically refetches sitemaps to keep content fresh. Serve your sitemap with `Last-Modified` or `ETag` response headers so the crawler can detect whether the sitemap has changed since the last fetch.

### Recommendations

* Include `<lastmod>` on all URLs to help identify which pages have changed. Use ISO 8601 format (for example, `2025-01-15T00:00:00+00:00`).
* Use sitemap index files for large sites with multiple sitemaps.
* Compress large sitemaps using `.gz` format to reduce bandwidth.
* Keep sitemaps under 50 MB and 50,000 URLs per file (standard sitemap limits).

## Related resources

* [FAQ: Will Browser Run be detected by Bot Management?](https://developers.cloudflare.com/browser-run/faq/#will-browser-run-be-detected-by-bot-management) — How Browser Run interacts with bot protection and how to create a WAF skip rule
* [Automatic request headers](https://developers.cloudflare.com/browser-run/reference/automatic-request-headers/) — User-Agent strings and non-configurable headers used by Browser Run

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/reference/robots-txt/","name":"robots.txt and sitemaps"}}]}
```

---

---
title: Supported fonts
description: Browser Run uses a managed Chromium environment that includes a standard set of fonts. When you generate a screenshot or PDF, text is rendered using the fonts available in this environment.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/reference/supported-fonts.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Supported fonts

Browser Run uses a managed Chromium environment that includes a standard set of fonts. When you generate a screenshot or PDF, text is rendered using the fonts available in this environment.

If your webpage specifies a font that is not supported yet, Chromium will automatically fall back to a similar supported font. If you would like to use a font that is not currently supported, refer to [Custom fonts](https://developers.cloudflare.com/browser-run/features/custom-fonts/).

## Pre-installed fonts

The following sections list the fonts available in the Browser Run environment.

### Generic CSS font family support

The following generic CSS font families are supported:

* `serif`
* `sans-serif`
* `monospace`
* `cursive`
* `fantasy`

### Common system fonts

* Andale Mono
* Arial
* Arial Black
* Comic Sans MS
* Courier
* Courier New
* Georgia
* Helvetica
* Impact
* Lucida Handwriting
* Times
* Times New Roman
* Trebuchet MS
* Verdana
* Webdings

### Open source and extended fonts

* Bitstream Vera (Serif, Sans, Mono)
* Cyberbit
* DejaVu (Serif, Sans, Mono)
* FreeFont (FreeSerif, FreeSans, FreeMono)
* GFS Neohellenic
* Liberation (Serif, Sans, Mono)
* Open Sans
* Roboto

### International fonts

Browser Run includes additional font packages for non-Latin scripts and emoji:

* IPAfont Gothic (Japanese)
* Indic fonts (Devanagari, Bengali, Tamil, and others)
* KACST fonts (Arabic)
* Noto CJK (Chinese, Japanese, Korean)
* Noto Color Emoji
* TLWG Thai fonts
* WenQuanYi Zen Hei (Chinese)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/reference/supported-fonts/","name":"Supported fonts"}}]}
```

---

---
title: Quick Actions timeouts
description: Browser Run uses several independent timers to manage how long different parts of a request can take. If any of these timers exceed their limit, the request returns a timeout error.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/reference/timeouts.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Quick Actions timeouts

Browser Run uses several independent timers to manage how long different parts of a request can take. If any of these timers exceed their limit, the request returns a timeout error.

Each timer controls a specific part of the rendering lifecycle — from page load, to selector load, to action.

| Timer                 | Scope                                                                                                                                      | Default          | Max   |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | ----- |
| goToOptions.timeout   | Time to wait for the page to load before timeout.                                                                                          | 30 s             | 60 s  |
| goToOptions.waitUntil | Determines when page load is considered complete. Refer to [waitUntil options](#waituntil-options) for details.                            | domcontentloaded | —     |
| waitForSelector       | Time to wait for a specific element (any CSS selector) to appear on the page.                                                              | null             | 60 s  |
| waitForTimeout        | Additional amount of time to wait after the page has loaded to proceed with actions.                                                       | null             | 60 s  |
| actionTimeout         | Time to wait for the action itself (for example: a screenshot, PDF, or scrape) to complete after the page has loaded.                      | null             | 5 min |
| PDFOptions.timeout    | Same as actionTimeout, but only applies to the [/pdf endpoint](https://developers.cloudflare.com/browser-run/quick-actions/pdf-endpoint/). | 30 s             | 5 min |

### `waitUntil` options

The `goToOptions.waitUntil` parameter controls when the browser considers page navigation complete. This is important for JavaScript-heavy pages where content is rendered dynamically after the initial page load.

| Value            | Behavior                                                                                       |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| load             | Waits for the load event, including all resources like images and stylesheets                  |
| domcontentloaded | Waits until the DOM content has been fully loaded, which fires before the load event (default) |
| networkidle0     | Waits until there are no network connections for at least 500 ms                               |
| networkidle2     | Waits until there are no more than two network connections for at least 500 ms                 |

For pages that rely on JavaScript to render content, use `networkidle0` or `networkidle2` to ensure the page is fully rendered before extraction.

## Notes and recommendations

You can set multiple timers — as long as one is complete, the request will fire.

If you are not getting the expected output:

* Try increasing `goToOptions.timeout` (up to 60 s).
* If waiting for a specific element, use `waitForSelector`. Otherwise, use `goToOptions.waitUntil` set to `networkidle2` to ensure the page has finished loading dynamic content.
* If you are getting a `422`, it may be the action itself (ex: taking a screenshot, extracting the html content) that takes a long time. Try increasing the `actionTimeout` instead.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/reference/timeouts/","name":"Quick Actions timeouts"}}]}
```

---

---
title: Wrangler
description: Use Wrangler, a command-line tool, to deploy projects using Cloudflare's Workers Browser Run API.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/reference/wrangler.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Wrangler

[Wrangler](https://developers.cloudflare.com/workers/wrangler/) is a command-line tool for building with Cloudflare developer products.

Use Wrangler to deploy projects that use the Workers Browser Run API.

## Install

To install Wrangler, refer to [Install and Update Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/).

## Bindings

[Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) allow your Workers to interact with resources on the Cloudflare developer platform. A browser binding will provide your Worker with an authenticated endpoint to interact with a dedicated Chromium browser instance.

To deploy a Browser Run Worker, you must declare a [browser binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) in your Worker's Wrangler configuration file.

Note

To enable built-in Node.js APIs and polyfills, add the nodejs\_compat compatibility flag to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). This also enables nodejs\_compat\_v2 as long as your compatibility date is 2024-09-23 or later. [Learn more about the Node.js compatibility flag and v2](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag).

* [  wrangler.jsonc ](#tab-panel-3596)
* [  wrangler.toml ](#tab-panel-3597)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  // Top-level configuration

  "name": "browser-rendering",

  "main": "src/index.ts",

  "workers_dev": true,

  "compatibility_flags": ["nodejs_compat_v2"],

  "browser": {

    "binding": "MYBROWSER",

  },

}


```

Explain Code

TOML

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "browser-rendering"

main = "src/index.ts"

workers_dev = true

compatibility_flags = [ "nodejs_compat_v2" ]


[browser]

binding = "MYBROWSER"


```

After the binding is declared, access the DevTools endpoint using `env.MYBROWSER` in your Worker code:

JavaScript

```

const browser = await puppeteer.launch(env.MYBROWSER);


```

Run `npx wrangler dev` to test your Worker locally.

### Headful mode (experimental)

By default, local development runs Chrome in headless mode. To launch Chrome in visible (headful) mode for debugging, set the `X_BROWSER_HEADFUL` environment variable:

Terminal window

```

X_BROWSER_HEADFUL=true npx wrangler dev


```

This opens a browser window on screen so you can watch navigations, interactions, and rendering in real time. Headful mode is for local development only and does not affect deployed Workers. This feature is experimental and may change without notice.

Note

When using [@cloudflare/playwright](https://developers.cloudflare.com/browser-run/playwright/), two Chrome windows may appear. This is expected behavior due to how Playwright handles browser contexts via CDP.

Use real headless browser during local development

To interact with a real headless browser during local development, set `"remote" : true` in the Browser binding configuration. Learn more in our [remote bindings documentation](https://developers.cloudflare.com/workers/development-testing/#remote-bindings).

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/reference/wrangler/","name":"Wrangler"}}]}
```

---

---
title: Wrangler commands
description: Manage Browser Run sessions from the command line using Wrangler.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/reference/wrangler-commands.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Wrangler commands

Use `wrangler browser` commands to manage Browser Run sessions from the command line.

## `browser create`

Create a new browser rendering session

* [  npm ](#tab-panel-3598)
* [  pnpm ](#tab-panel-3599)
* [  yarn ](#tab-panel-3600)

Terminal window

```

npx wrangler browser create


```

Terminal window

```

pnpm wrangler browser create


```

Terminal window

```

yarn wrangler browser create


```

* `--lab` ` boolean ` default: false  
Enable lab browser session with experimental Chrome features (e.g., WebMCP)
* `--keepAlive` ` number ` alias: --k  
Keep-alive duration in seconds (60-600)
* `--json` ` boolean ` default: false  
Return session info as JSON
* `--open` ` boolean `  
Open DevTools in browser (default: true in interactive mode)

Global flags

* `--v` ` boolean ` alias: --version  
Show version number
* `--cwd` ` string `  
Run as if Wrangler was started in the specified directory instead of the current working directory
* `--config` ` string ` alias: --c  
Path to Wrangler configuration file
* `--env` ` string ` alias: --e  
Environment to use for operations, and for selecting .env and .dev.vars files
* `--env-file` ` string `  
Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files
* `--experimental-provision` ` boolean ` aliases: --x-provision default: true  
Experimental: Enable automatic resource provisioning
* `--experimental-auto-create` ` boolean ` alias: --x-auto-create default: true  
Automatically provision draft bindings with new resources

## `browser close`

Close a browser rendering session

* [  npm ](#tab-panel-3601)
* [  pnpm ](#tab-panel-3602)
* [  yarn ](#tab-panel-3603)

Terminal window

```

npx wrangler browser close [SESSIONID]


```

Terminal window

```

pnpm wrangler browser close [SESSIONID]


```

Terminal window

```

yarn wrangler browser close [SESSIONID]


```

* `[SESSIONID]` ` string ` required  
The session ID to close
* `--json` ` boolean ` default: false  
Return result as JSON

Global flags

* `--v` ` boolean ` alias: --version  
Show version number
* `--cwd` ` string `  
Run as if Wrangler was started in the specified directory instead of the current working directory
* `--config` ` string ` alias: --c  
Path to Wrangler configuration file
* `--env` ` string ` alias: --e  
Environment to use for operations, and for selecting .env and .dev.vars files
* `--env-file` ` string `  
Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files
* `--experimental-provision` ` boolean ` aliases: --x-provision default: true  
Experimental: Enable automatic resource provisioning
* `--experimental-auto-create` ` boolean ` alias: --x-auto-create default: true  
Automatically provision draft bindings with new resources

## `browser list`

List active browser rendering sessions

* [  npm ](#tab-panel-3604)
* [  pnpm ](#tab-panel-3605)
* [  yarn ](#tab-panel-3606)

Terminal window

```

npx wrangler browser list


```

Terminal window

```

pnpm wrangler browser list


```

Terminal window

```

yarn wrangler browser list


```

* `--json` ` boolean ` default: false  
Return output as JSON

Global flags

* `--v` ` boolean ` alias: --version  
Show version number
* `--cwd` ` string `  
Run as if Wrangler was started in the specified directory instead of the current working directory
* `--config` ` string ` alias: --c  
Path to Wrangler configuration file
* `--env` ` string ` alias: --e  
Environment to use for operations, and for selecting .env and .dev.vars files
* `--env-file` ` string `  
Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files
* `--experimental-provision` ` boolean ` aliases: --x-provision default: true  
Experimental: Enable automatic resource provisioning
* `--experimental-auto-create` ` boolean ` alias: --x-auto-create default: true  
Automatically provision draft bindings with new resources

## `browser view`

View a live browser session

* [  npm ](#tab-panel-3607)
* [  pnpm ](#tab-panel-3608)
* [  yarn ](#tab-panel-3609)

Terminal window

```

npx wrangler browser view [SESSIONID]


```

Terminal window

```

pnpm wrangler browser view [SESSIONID]


```

Terminal window

```

yarn wrangler browser view [SESSIONID]


```

* `[SESSIONID]` ` string `  
The session ID to inspect (optional if only one session exists)
* `--target` ` string `  
Target selector (matches id exactly, or url/title by substring)
* `--json` ` boolean ` default: false  
Return live browser session URL(s) as JSON
* `--open` ` boolean `  
Open in browser (default: true in interactive mode)

Global flags

* `--v` ` boolean ` alias: --version  
Show version number
* `--cwd` ` string `  
Run as if Wrangler was started in the specified directory instead of the current working directory
* `--config` ` string ` alias: --c  
Path to Wrangler configuration file
* `--env` ` string ` alias: --e  
Environment to use for operations, and for selecting .env and .dev.vars files
* `--env-file` ` string `  
Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files
* `--experimental-provision` ` boolean ` aliases: --x-provision default: true  
Experimental: Enable automatic resource provisioning
* `--experimental-auto-create` ` boolean ` alias: --x-auto-create default: true  
Automatically provision draft bindings with new resources

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/reference/wrangler-commands/","name":"Wrangler commands"}}]}
```
