---
title: Browser Rendering
description: Control headless browsers with Cloudflare's Workers Browser Rendering 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-rendering/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Browser Rendering

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 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-rendering/rest-api/markdown-endpoint/)
* [Screenshots](https://developers.cloudflare.com/browser-rendering/rest-api/screenshot-endpoint/)
* [PDFs](https://developers.cloudflare.com/browser-rendering/rest-api/pdf-endpoint/)
* [Snapshots](https://developers.cloudflare.com/browser-rendering/rest-api/snapshot/)
* [Links](https://developers.cloudflare.com/browser-rendering/rest-api/links-endpoint/)
* [HTML elements](https://developers.cloudflare.com/browser-rendering/rest-api/scrape-endpoint/)
* [Structured data](https://developers.cloudflare.com/browser-rendering/rest-api/json-endpoint/)
* [Crawled web content](https://developers.cloudflare.com/browser-rendering/rest-api/crawl-endpoint/)

## Integration methods

Browser Rendering offers multiple integration methods depending on your use case:

* **[REST API](https://developers.cloudflare.com/browser-rendering/rest-api/)**: Simple HTTP endpoints for stateless tasks like screenshots, PDFs, and scraping.
* **[Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/)**: Full browser automation within Workers using [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/), or [Stagehand](https://developers.cloudflare.com/browser-rendering/stagehand/).

| Use case                          | Recommended                                                                                                                                                  | Why                                               |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
| Simple screenshot, PDF, or scrape | [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/)                                                                                    | No code deployment; single HTTP request           |
| Browser automation                | [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/)                                                                                | Full control with built-in tracing and assertions |
| Porting existing scripts          | [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/) or [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/) | Minimal code changes from standard libraries      |
| AI-powered data extraction        | [JSON endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/json-endpoint/)                                                                 | Structured data via natural language prompts      |
| AI agent browsing                 | [Playwright MCP](https://developers.cloudflare.com/browser-rendering/playwright/playwright-mcp/)                                                             | LLMs control browsers via MCP                     |
| Resilient scraping                | [Stagehand](https://developers.cloudflare.com/browser-rendering/stagehand/)                                                                                  | AI finds elements by intent, not selectors        |

## 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**: [REST APIs](https://developers.cloudflare.com/browser-rendering/rest-api/) for common actions, while [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/) and [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/) provide familiar automation libraries for complex workflows
* **Session management**: [Reuse browser sessions](https://developers.cloudflare.com/browser-rendering/workers-bindings/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-rendering/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-rendering/workers-bindings/browser-rendering-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-rendering/playwright/playwright-mcp/) or [Stagehand](https://developers.cloudflare.com/browser-rendering/stagehand/).

## More resources

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

Choose between REST API and Workers Bindings, then deploy your first project.

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

Learn about Browser Rendering limits.

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

Learn about Browser Rendering pricing.

[Playwright API](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}}]}
```

---

---
title: Get started
description: Cloudflare 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-rendering/get-started.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Get started

Cloudflare 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 Rendering offers multiple integration methods depending on your use case:

* **[REST API](https://developers.cloudflare.com/browser-rendering/rest-api/)**: Simple HTTP endpoints for stateless tasks like screenshots, PDFs, and scraping.
* **[Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/)**: Full browser automation within Workers using [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/), or [Stagehand](https://developers.cloudflare.com/browser-rendering/stagehand/).

| Use case                          | Recommended                                                                                                                                                  | Why                                               |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
| Simple screenshot, PDF, or scrape | [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/)                                                                                    | No code deployment; single HTTP request           |
| Browser automation                | [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/)                                                                                | Full control with built-in tracing and assertions |
| Porting existing scripts          | [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/) or [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/) | Minimal code changes from standard libraries      |
| AI-powered data extraction        | [JSON endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/json-endpoint/)                                                                 | Structured data via natural language prompts      |
| AI agent browsing                 | [Playwright MCP](https://developers.cloudflare.com/browser-rendering/playwright/playwright-mcp/)                                                             | LLMs control browsers via MCP                     |
| Resilient scraping                | [Stagehand](https://developers.cloudflare.com/browser-rendering/stagehand/)                                                                                  | AI finds elements by intent, not selectors        |

## REST API

### 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"


```

The REST API can also be used to:

* [Fetch HTML](https://developers.cloudflare.com/browser-rendering/rest-api/content-endpoint/)
* [Generate a PDF](https://developers.cloudflare.com/browser-rendering/rest-api/pdf-endpoint/)
* [Explore all REST API endpoints](https://developers.cloudflare.com/browser-rendering/rest-api/)

## Workers Bindings

### 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-rendering/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 Rendering 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 Rendering API binding and the KV namespaces you created:

* [  wrangler.jsonc ](#tab-panel-3238)
* [  wrangler.toml ](#tab-panel-3239)

```

{

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

  "name": "browser-worker",

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

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": [

    "nodejs_compat"

  ],

  "browser": {

    "binding": "MYBROWSER"

  },

  "kv_namespaces": [

    {

      "binding": "BROWSER_KV_DEMO",

      "id": "22cf855786094a88a6906f8edac425cd",

      "preview_id": "e1f8b68b68d24381b57071445f96e623"

    }

  ]

}


```

```

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

name = "browser-worker"

main = "src/index.js"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[browser]

binding = "MYBROWSER"


[[kv_namespaces]]

binding = "BROWSER_KV_DEMO"

id = "22cf855786094a88a6906f8edac425cd"

preview_id = "e1f8b68b68d24381b57071445f96e623"


```

#### 5\. Code

* [  JavaScript ](#tab-panel-3236)
* [  TypeScript ](#tab-panel-3237)

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");

    }

  },

};


```

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>;


```

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 [REST API endpoints](https://developers.cloudflare.com/browser-rendering/rest-api/)
* Try out the [Playwright MCP](https://developers.cloudflare.com/browser-rendering/playwright/playwright-mcp/)
* Learn more about Browser Rendering [limits](https://developers.cloudflare.com/browser-rendering/limits/) and [pricing](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/get-started/","name":"Get started"}}]}
```

---

---
title: Examples
description: Use these REST API examples to perform quick, common tasks.
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-rendering/examples.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Examples

## REST API examples

Use these [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) examples to perform quick, common tasks.

[ Fetch rendered HTML from a URL ](https://developers.cloudflare.com/browser-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/links-endpoint/#get-all-links-on-a-page) Retrieve all links from a webpage, including hidden ones. 

## Workers Bindings examples

Use [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/) for dynamic, multi-step browser automation with [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/), or [Stagehand](https://developers.cloudflare.com/browser-rendering/stagehand/).

[ Get page metrics with Puppeteer ](https://developers.cloudflare.com/browser-rendering/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-rendering/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-rendering/playwright/#assertions) Use Playwright assertions to test web applications in a Worker. 

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

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

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

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

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

---

---
title: REST API
description: The REST API is a RESTful interface that provides endpoints for common browser actions such as capturing screenshots, extracting HTML content, generating PDFs, and more.
The following are the available options:
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-rendering/rest-api/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# REST API

The REST API is a RESTful interface that provides endpoints for common browser actions such as capturing screenshots, extracting HTML content, generating PDFs, and more. The following are the available options:

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

Use the REST API 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. If you require more advanced automation, custom workflows, or persistent browser sessions, [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/) are the better choice.

## 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 Rendering usage in two ways:

* In the Cloudflare dashboard, go to the **Browser Rendering** page to view aggregate metrics, including total REST API requests and total browser hours used.[ Go to **Browser Rendering** ](https://dash.cloudflare.com/?to=/:account/workers/browser-rendering)
* `X-Browser-Ms-Used` header: Returned in every REST API 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}}]}
```

---

---
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-rendering/rest-api/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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-rendering/rest-api/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 the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-3258)
* [ TypeScript SDK ](#tab-panel-3259)

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);


```

## Advanced usage

Looking for more parameters?

Visit the [Browser Rendering 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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/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 Rendering will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Rendering requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-rendering/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 Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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-rendering/rest-api/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 the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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-rendering/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-rendering/rest-api/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-rendering/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");

}


```

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

}


```

### 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-rendering/rest-api/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. For the full list, refer to the [API docs](https://developers.cloudflare.com/api/resources/browser%5Frendering/).

| 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-rendering/rest-api/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-rendering/rest-api/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 Rendering 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/*"

      ]

    }

}'


```

## Advanced usage

Looking for more parameters?

Visit the [Browser Rendering 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/**"

      ]

    }

  }'


```

### 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-rendering/rest-api/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/*"

      ]

    }

  }'


```

### 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"

    }

  }'


```

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"

    }

  }'


```

### 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

    }

  }'


```

### Block unnecessary resources

Speed up crawling by blocking images and media:

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"

    ]

  }'


```

## 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-rendering/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-rendering/reference/robots-txt/#blocking-crawlers-with-robotstxt).

Bot protection may block crawling

Browser Rendering 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 Rendering crawler.

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

### User-Agent

The `/crawl` endpoint uses `CloudflareBrowserRenderingCrawler/1.0` as its User-Agent, which is different from the other [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) endpoints. This User-Agent is not customizable. Unlike the other REST API endpoints and [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/), 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-rendering/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-rendering/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-rendering/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-rendering/rest-api/json-endpoint/#using-a-custom-model-byo-api-key) for details.

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

## Troubleshooting

If you have questions or encounter an error, see the [Browser Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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-rendering/rest-api/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-rendering/rest-api/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 the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-3260)
* [ TypeScript SDK ](#tab-panel-3261)

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"

              ]

            }

          }

        }

      }

  }

}'


```

```

{

  "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/"

      }

    ]

  }

}


```

### 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"

    ]

  }

}


```

### 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"

              ]

            }

          }

        }

      }

  }'


```

```

{

  "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/"

      }

    ]

  }

}


```

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);


```

## Advanced Usage

Looking for more parameters?

Visit the [Browser Rendering 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 Rendering 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 Rendering to call the provider on your behalf.

This example uses the `custom_ai` parameter to instruct Browser Rendering 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>"

    }

  ]

}


```

```

{

  "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 Rendering will attempt the models in order until one succeeds. To add failover, list additional models in the `custom_ai` array.

In this example, Browser Rendering 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>"

  }

]


```

## 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-rendering/rest-api/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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/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 Rendering will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Rendering requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-rendering/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 Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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-rendering/rest-api/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 the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-3262)
* [ TypeScript SDK ](#tab-panel-3263)

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/"

  ]

}


```

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);


```

## Advanced usage

Looking for more parameters?

Visit the [Browser Rendering 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/"

  ]

}


```

### 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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/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 Rendering will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Rendering requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-rendering/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 Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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-rendering/rest-api/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 the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-3264)
* [ TypeScript SDK ](#tab-panel-3265)

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);


```

### 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 Rendering 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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/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 Rendering will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Rendering requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-rendering/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 Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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 rendering 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-rendering/rest-api/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 rendering service.

Before you begin, make sure you [create a custom API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-3266)
* [ TypeScript SDK ](#tab-panel-3267)

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"


```

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);


```

### 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"


```

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 Rendering 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"


```

### 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"


```

### 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"


```

### Use custom fonts

If your PDF requires a font that is not pre-installed in the Browser Rendering 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-rendering/features/custom-fonts/#rest-api).

### 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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/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 Rendering will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Rendering requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-rendering/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 Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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-rendering/rest-api/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 the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-3268)
* [ TypeScript SDK ](#tab-panel-3269)

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"

  }]

}'


```

```

{

  "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"

    }

  ]

}


```

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);


```

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 Rendering 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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/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 Rendering will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Rendering requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-rendering/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 Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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-rendering/rest-api/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 the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-3270)
* [ TypeScript SDK ](#tab-panel-3271)

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"


```

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);


```

### 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 Rendering 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 Rendering supports three authentication methods, which work across all [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) endpoints. For a quick reference of all methods, refer to [How do I render authenticated pages using the REST API?](https://developers.cloudflare.com/browser-rendering/faq/#how-do-i-render-authenticated-pages-using-the-rest-api).

#### 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"


```

#### 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"


```

#### 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"


```

### 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"


```

### 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"


```

### 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"


```

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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/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 Rendering will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Rendering requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-rendering/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 Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/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-rendering/rest-api/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 the `Browser Rendering - Edit` permission. For more information, refer to [REST API — Before you begin](https://developers.cloudflare.com/browser-rendering/rest-api/#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-3272)
* [ TypeScript SDK ](#tab-panel-3273)

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);


```

## Advanced usage

Looking for more parameters?

Visit the [Browser Rendering 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

    }

  }'


```

```

{

  "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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/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 Rendering will always be identified as a bot. Because the User-Agent is configurable, destination servers looking to identify or block Browser Rendering requests should use the [non-configurable headers](https://developers.cloudflare.com/browser-rendering/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 Rendering FAQ and troubleshooting guide](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/rest-api/","name":"REST API"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/rest-api/snapshot/","name":"/snapshot - Take a webpage snapshot"}}]}
```

---

---
title: Workers Bindings
description: Workers Bindings allow you to execute advanced browser rendering scripts within Cloudflare Workers. They provide developers the flexibility to automate and control complex workflows and browser interactions. The following options are available for browser rendering tasks:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

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

Was this helpful?

YesNo

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

Copy page

# Workers Bindings

Workers Bindings allow you to execute advanced browser rendering scripts within Cloudflare Workers. They provide developers the flexibility to automate and control complex workflows and browser interactions. The following options are available for browser rendering tasks:

* [ Deploy a Browser Rendering Worker ](https://developers.cloudflare.com/browser-rendering/workers-bindings/screenshots/)
* [ Deploy a Browser Rendering Worker with Durable Objects ](https://developers.cloudflare.com/browser-rendering/workers-bindings/browser-rendering-with-do/)
* [ Reuse sessions ](https://developers.cloudflare.com/browser-rendering/workers-bindings/reuse-sessions/)

Use Workers Bindings when you need advanced browser automation, custom workflows, or complex interactions beyond basic rendering. For quick, one-off tasks like capturing screenshots or extracting HTML, the [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) is the simpler choice.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/workers-bindings/","name":"Workers Bindings"}}]}
```

---

---
title: Deploy a Browser Rendering Worker with Durable Objects
description: Use the Browser Rendering 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-rendering/workers-bindings/browser-rendering-with-DO.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Deploy a Browser Rendering Worker with Durable Objects

**Last reviewed:**  over 2 years ago 

By following this guide, you will create a Worker that uses the Browser Rendering 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-rendering/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 Rendering 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-3276)
* [  wrangler.toml ](#tab-panel-3277)

```

{

  "$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-03",

  "compatibility_flags": [

    "nodejs_compat"

  ],

  "account_id": "<ACCOUNT_ID>",

  // Browser Rendering 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"

      ]

    }

  ]

}


```

```

"$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-03"

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" ]


```

## 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-3278)
* [  TypeScript ](#tab-panel-3279)

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+CAngLzbPYZb6HbW5QDGU2AAwB2AIyjhAZgCsAFgBMw2bIBcLFm2Ac4XGnwEjxUuYuUBYAFABhdFQgBTO9gAiUAM4x0bqNFsqSmngExCRUcMD2DABEUDT2AB4AdABWblGkqFBgjuGRMXFJqVGWNnaOENgAKnQw9v5wMDBgfARQtsjJcABucG68CLAQANTA6Ljg9paWUMCeSCQA3iTOIAhwZNkA8mTJ9rwQJAC+AQjowCRRvGCUuKhgiHUA7pgA1vYIaUQWM3MHAFQkXokGAgRr2BzvE5nC4AASuNzuD2QILBEIQ6QsFgSvxIuHsqDg4AOCwsJEBbjoVF4AXBvAAFgAKBD2ACOIHsbggABoSI4ugBKRakskkXi2TkkdA7EgMXlULqJABCACVNgB1ADKAFFlYkAObgxV0AByuQZUTIp0ebneUX5X2FZOQyBIGscuEBJGZbI5Bwg6BIEDp9mWq3W2RI212+x5QccgMecB8JB8bi9HM8VBtjtF4oOzI8MoTSYOUuSiVQtMZ3vZnPtmJF6Ygqyo6Y8XzJhy5FkODrFWYOAGktVqAAoAfRV6u1yvHAEEADIASQAalrx0vjePtVZNsbnBqiwA2QQO7GYA5XXppxVWm0IXnxBw0NMrNYbexRvbE4WW9DW94OxIN4YAgOdmi6ewlyoN1+1wNNZVPYVOUwOADQdMl+05BAQH2TAGU5Ah7B5PlBRJRs3FBd4CMIBwSPletGyDdxEhQtYDSLQiHFY-12PsIDDgbclKWpSsIHpJlWVrCAyJzZ1XX6ex4wLdAwBAXws0DAMHAlSgIBzLCDkeKBcCDIsAG1RAATnkQQeVESQjyPezpEcnlHLskhZFEWQAF0gMwvMSGDKA9TpA5ZUswQAA5POEI9op5aKj1kHkUs86KrKPfyhKdF0AFUbUDYNRVWZk7FxIjARoQMZhDf1RWZKq4ACVS8QfbDcObZlWofZV5AMoKqH-ZwqtlKh7EeZYiIZRiRUM3N8VQItREENaSABE8NpIaQAtzAcvUoOJcFGhwiwmqbTvsBkczJABZAg6USU5uAZYbHiu-VwUqOrZpIF0xSWwUAUB1BUG7Rt+USf0NQgAYqD1Wa9oWjAwHaosXuOz6PGaCBzRIABxO7KjtczBByuSXSXZa4x69xPT-ACOo5bxbElWoqB5ZkuCKnwcygZaGQAQmYtxEkZ+8SAAHylkgRbpFiJfeRJ3FKCb9nsXBZtkxtAqzVT7ESa5EYAA1vf9JecTZ-FhxBoAR0JJpTAc4CpewTbmpj6CFXWRVF8W70hWU4ETZMUVqNEjcJKlGX9vlEjugBNKdNR1T2RWOXgCHpEgGXsHXff2twDaN9BEdu3WzcDh8rf8GxwA9YaDkI+YlYfWJCLdxISC1BBTgQfwABIFnsQ4TYhwv087HNBMpkhlQ5cFgPsUDwKgSDAVQBwH3sOAc6zsAwC04qQytnN-ZAsCIKgmC9lseCiyQuerGDXgXhTZa28fdwIDTMh8UwCGdAHNYh6mBGhew-NBby0VtXfkFdmTdVbBdeeGZxTXSiObJmJB7jUBzgSLImsog8iWFxEAbh-DSHWocesg0DrwA4sHUOfoFZizbokC6o4IFI1yv9F0lQ4BvBIH0Zkjg3B0nQL-SUy1d45xEUpVs3gABekDGxsFztkA4UBH6kG0QAHhIMZUyT1sgIyDLooYQwC6NhDiWcBBpWLghXFASavwGRLCMUGfwni6TmSgL5HkIUwoQH8EE8KfjfJHCnsWMOED9SSPQOacKEAYAUOdM8BAbwPiJHhCAW49xmQ5LOMgO0e09YSkyNkU0EQiwm3kWIiREBxzDx8REw48Rh5hIgG0k2ZSi7N2pEwuxDDDb1KzI0nhFdbHJjjvKJU+UrDDkqIkEEeMTbD1Ru1Q4yANmEOqaPFIMA9Tj2EbwdOs9GzySsNcIqhAyCGODK2WmIZ6bDRIKMHqGT34NX-riWwQCnklRGTmaZBwRk5JuddWhlyXQLxtAcS+a8N5wC3pCWoCBNCgMDL0F4aYGpxmWJsc+rDEiIuvtBWC98EIkCfjC10S8CWZA+AcOABTzgNTeCvQlgJr50IlLwMq5RwKIHOEMmZJK2JxINFfEVPDGwC1zgKvuQq2UynGuAMA1j5rihLsbBkVcLaQlrsI8E9swGspFR7PpC1Khai3DuPcB4VqCG2qtNafTQXFRYpKhx8LhVYAZJ9d6f0hgkFtfarUu59wanObwxBLZHZTThZmG05pKK8F4CzUpwoLnCSpDy2VWqL4ryvuvG+lKXwkCGLKVaGERTyS1E+d05Bq7cuaJWKBudi2r3JbfOCaYDHDjHJOVUqdZyLlXOuTc25I2OpjT7XWWFdVlxuoXEgBrsHGrpECf+8ZL48rLb1Egw9u2lsghSu+L5jg2n7d3Rtz4eAO3bRmV2iRx4V2iZ6-2PrRngn9cAQNREOH-hDSQVarq1qCGifJROlBRSuzlJRHqBLrT7XVhpFMaZ92ssPWQOg6YfScixcXCIzEEYV3kpgEgyRyEHE0SmS8kLATqTOK0A+YB8OPEeSfOmaY3lfOPr83A-yKMunUQkcITRiLrq-SS9hkEPhtCoLNE2M9eRgCKuRRdOrsil3LmujdltraPkzZrTWOCBZAOWsPIdE4U4znnMuNcG4I1RoPIcMW77fbRIVQyf2bctWNiXbpvVJtrleCxewq1FcySydgYahAEKvBQr6YJRsaWjg9ksOoZgmhtC6B4PwIQYgJAyAUEoWQJRbDPgqK4DwEWNL+ECNoUgYQIjRFI3AbQ6QAiELa3kDYUpijWGq+UKoNQ6iAkaM0LOGkOjFyoFMCwCwojACTFQccoxxjZCiCofIeJChpEONlnLeXggFf0MVowZXTCyGYJYIAA)

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();

      }

    }

  }

}


```

## 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/general/#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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/workers-bindings/","name":"Workers Bindings"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/workers-bindings/browser-rendering-with-do/","name":"Deploy a Browser Rendering Worker with Durable Objects"}}]}
```

---

---
title: Reuse sessions
description: The best way to improve the performance of your browser rendering Worker is to reuse sessions. One way to do that is via Durable Objects, which allows you to keep a long running connection from a Worker to a browser. Another way is to keep the browser open after you've finished with it, and connect to that session each time you have a new 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-rendering/workers-bindings/reuse-sessions.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Reuse sessions

The best way to improve the performance of your browser rendering Worker is to reuse sessions. One way to do that is via [Durable Objects](https://developers.cloudflare.com/browser-rendering/workers-bindings/browser-rendering-with-do/), which allows you to keep a long running connection from a Worker to a browser. Another way is to keep the browser open after you've finished with it, and connect to that session each time you have a new request.

In short, this entails using `browser.disconnect()` instead of `browser.close()`, and, if there are available sessions, using `puppeteer.connect(env.MY_BROWSER, sessionID)` instead of launching a new browser session.

## 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-rendering/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-3280)
* [  wrangler.toml ](#tab-panel-3281)

```

{

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

  "name": "browser-worker",

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

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": [

    "nodejs_compat"

  ],

  "browser": {

    "binding": "MYBROWSER"

  }

}


```

```

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

name = "browser-worker"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

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-rendering/limits/), it will close automatically, so you must have enough requests per minute to keep it alive.

* [  JavaScript ](#tab-panel-3282)
* [  TypeScript ](#tab-panel-3283)

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;

  },

};


```

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!;

  },

};


```

Besides `puppeteer.sessions()`, we have added other methods to facilitate [Session Management](https://developers.cloudflare.com/browser-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/workers-bindings/","name":"Workers Bindings"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/workers-bindings/reuse-sessions/","name":"Reuse sessions"}}]}
```

---

---
title: Deploy a Browser Rendering Worker
description: By following this guide, you will create a Worker that uses the Browser Rendering 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-rendering/workers-bindings/screenshots.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Deploy a Browser Rendering Worker

By following this guide, you will create a Worker that uses the Browser Rendering 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-rendering/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 Rendering 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 Rendering API binding and the KV namespaces you created:

* [  wrangler.jsonc ](#tab-panel-3286)
* [  wrangler.toml ](#tab-panel-3287)

```

{

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

  "name": "browser-worker",

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

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": [

    "nodejs_compat"

  ],

  "browser": {

    "binding": "MYBROWSER"

  },

  "kv_namespaces": [

    {

      "binding": "BROWSER_KV_DEMO",

      "id": "22cf855786094a88a6906f8edac425cd",

      "preview_id": "e1f8b68b68d24381b57071445f96e623"

    }

  ]

}


```

```

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

name = "browser-worker"

main = "src/index.js"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[browser]

binding = "MYBROWSER"


[[kv_namespaces]]

binding = "BROWSER_KV_DEMO"

id = "22cf855786094a88a6906f8edac425cd"

preview_id = "e1f8b68b68d24381b57071445f96e623"


```

#### 5\. Code

* [  JavaScript ](#tab-panel-3284)
* [  TypeScript ](#tab-panel-3285)

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");

    }

  },

};


```

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>;


```

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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/workers-bindings/","name":"Workers Bindings"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/workers-bindings/screenshots/","name":"Deploy a Browser Rendering Worker"}}]}
```

---

---
title: Playwright
description: Learn how to use Playwright with Cloudflare Workers for browser automation. Access Playwright API, manage sessions, and optimize 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-rendering/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 Rendering](https://developers.cloudflare.com/browser-rendering/).

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.2.0 ↗](https://github.com/cloudflare/playwright/releases/tag/v1.2.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-rendering/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-3248)
* [  wrangler.toml ](#tab-panel-3249)

```

{

  "$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-03",

  "upload_source_maps": true,

  "browser": {

    "binding": "MYBROWSER"

  }

}


```

```

"$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-03"

upload_source_maps = true


[browser]

binding = "MYBROWSER"


```

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",

      },

    });

  },

};


```

### 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",

      },

    });

  },

};


```

### 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),

      ),

    );

  },

};


```

### 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-3250)
* [  wrangler.toml ](#tab-panel-3251)

```

{

  "name": "storage-state-examples",

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

  "compatibility_flags": ["nodejs_compat"],

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "browser": {

    "binding": "MYBROWSER"

  },

  "kv_namespaces": [

    {

      "binding": "KV",

      "id": "<YOUR-KV-NAMESPACE-ID>"

    }

  ]

}


```

```

name = "storage-state-examples"

main = "src/index.ts"

compatibility_flags = [ "nodejs_compat" ]

# Set this to today's date

compatibility_date = "2026-04-03"


[browser]

binding = "MYBROWSER"


[[kv_namespaces]]

binding = "KV"

id = "<YOUR-KV-NAMESPACE-ID>"


```

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));


```

### 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-rendering/workers-bindings/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-rendering/limits/#is-there-a-maximum-session-duration) for more information.

### Session Reuse

The best way to improve the performance of your browser rendering Worker is to reuse sessions by keeping the browser open after you've 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();

  }

}


```

### 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 Rendering will always be identified as a bot.

## 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

  }

]


```

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

  }

]


```

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-rendering/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-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/playwright/","name":"Playwright"}}]}
```

---

---
title: Playwright MCP
description: Deploy a Playwright MCP server that uses Browser Rendering 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-rendering/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 Rendering.

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 rendering](https://developers.cloudflare.com/browser-rendering/) 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-3252)
* [  wrangler.toml ](#tab-panel-3253)

```

{

  "$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-03",

  "compatibility_flags": [

    "nodejs_compat"

  ],

  "browser": {

    "binding": "BROWSER"

  },

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": [

        "PlaywrightMCP"

      ]

    }

  ],

  "durable_objects": {

    "bindings": [

      {

        "name": "MCP_OBJECT",

        "class_name": "PlaywrightMCP"

      }

    ]

  }

}


```

```

"$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-03"

compatibility_flags = [ "nodejs_compat" ]


[browser]

binding = "BROWSER"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "PlaywrightMCP" ]


[[durable_objects.bindings]]

name = "MCP_OBJECT"

class_name = "PlaywrightMCP"


```

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 });

    }

  },

};


```

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

![alt text](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.

* Navigate to [https://playground.ai.cloudflare.com/ ↗](https://playground.ai.cloudflare.com/)
* Ensure that the model is set to `llama-3.3-70b-instruct-fp8-fast`
* In **MCP Servers**, set **URL** to `https://[my-mcp-url].workers.dev/sse`
* Click **Connect**
* 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, e.g. "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 our 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/playwright/","name":"Playwright"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/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 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-rendering/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 Rendering 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.0.4 ↗](https://github.com/cloudflare/puppeteer/releases/tag/v1.0.4), 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-rendering/reference/wrangler/#bindings) is configured and the `@cloudflare/puppeteer` library is installed, Puppeteer can be used in a Worker:

* [  JavaScript ](#tab-panel-3254)
* [  TypeScript ](#tab-panel-3255)

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);

  },

};


```

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>;


```

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-rendering/workers-bindings/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-rendering/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 Rendering will always be identified as a bot.

## 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

  );

});


```

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

  }

]


```

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

  }

]


```

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-rendering/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-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/puppeteer/","name":"Puppeteer"}}]}
```

---

---
title: Stagehand
description: Deploy a Stagehand server that uses Browser Rendering 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-rendering/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 Rendering, and [Workers AI](https://developers.cloudflare.com/workers-ai/) to automate a web task.

Note

Browser Rendering 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-rendering/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 Rendering 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-3274)
* [  wrangler.toml ](#tab-panel-3275)

```

  {

    "name": "stagehand-example",

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

    "compatibility_flags": ["nodejs_compat"],

    // Set this to today's date

    "compatibility_date": "2026-04-03",

    "observability": {

      "enabled": true

    },

    "browser": {

      "binding": "BROWSER"

    },

    "ai": {

      "binding": "AI"

    }

  }


```

```

name = "stagehand-example"

main = "src/index.ts"

compatibility_flags = [ "nodejs_compat" ]

# Set this to today's date

compatibility_date = "2026-04-03"


[observability]

enabled = true


[browser]

binding = "BROWSER"


[ai]

binding = "AI"


```

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:

```

{

  // ...

  "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);

  },

};


```

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"

    // ...

  }

}


```

### 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/stagehand/","name":"Stagehand"}}]}
```

---

---
title: FAQ
description: Below you will find answers to our most commonly asked questions about 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-rendering/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 Rendering.

For pricing questions, visit the [pricing FAQ](https://developers.cloudflare.com/browser-rendering/pricing/#pricing-faq). For usage limits questions, visit the [limits FAQ](https://developers.cloudflare.com/browser-rendering/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-rendering/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-rendering/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 Rendering wasn't 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-rendering/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 [REST API timeouts reference](https://developers.cloudflare.com/browser-rendering/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 Rendering captures the output.

JavaScript-heavy pages and Single Page Applications (SPAs) often load content dynamically after the initial HTML is parsed. By default, Browser Rendering 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.       |

REST API 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 [REST API timeouts](https://developers.cloudflare.com/browser-rendering/reference/timeouts/).

---

## Getting started & Development

### Does local development support all Browser Rendering features?

Not yet. Local development currently has the following limitation(s):

* Requests larger than 1 MB are not supported.

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 the REST API?

If the page you are rendering requires authentication, you can pass credentials using one of the following methods. These parameters work with all [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) 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

    }

  ]

}


```

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-rendering/rest-api/screenshot-endpoint/#capture-a-screenshot-of-an-authenticated-page).

### Will Browser Rendering be detected by Bot Management?

Yes, Browser Rendering 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 Rendering to access your website freely without your bot protection configuration interfering, you can create a WAF skip rule to [allowlist Browser Rendering](https://developers.cloudflare.com/browser-rendering/faq/#can-i-allowlist-browser-rendering-on-my-own-website).

### Can I allowlist Browser Rendering on my own website?

You must be on an Enterprise plan to allowlist Browser Rendering on your own website because WAF custom rules require access to [Bot Management](https://developers.cloudflare.com/bots/get-started/bot-management/) fields.

Browser Rendering uses different [bot detection IDs](https://developers.cloudflare.com/browser-rendering/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 Rendering`.
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-rendering/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 Rendering rotate IP addresses for outbound requests?

No. Browser Rendering 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-rendering/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 Rendering?

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 both the [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) and [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/). For instructions and examples, refer to [Custom fonts](https://developers.cloudflare.com/browser-rendering/features/custom-fonts/).

### How can I manage concurrency and session isolation with Browser Rendering?

If you are hitting concurrency [limits](https://developers.cloudflare.com/browser-rendering/limits/#workers-paid), or want to optimize concurrent browser usage with the [Workers Binding method](https://developers.cloudflare.com/browser-rendering/workers-bindings/), 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-rendering/workers-bindings/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 both the [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) and [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/) (using `@cloudflare/puppeteer` or `@cloudflare/playwright`).

### Is there any temporary caching of submitted content?

For the [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/), 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 [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/), 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/faq/","name":"FAQ"}}]}
```

---

---
title: Limits
description: Learn about the limits associated with 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-rendering/limits.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Limits

Browser Rendering limits are based on your [Cloudflare Workers plan](https://developers.cloudflare.com/workers/platform/pricing/).

For pricing information, refer to [Browser Rendering pricing](https://developers.cloudflare.com/browser-rendering/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 (Workers Bindings only) [1](#user-content-fn-1) | 3 per account                      |
| New browser instances (Workers Bindings only)                                   | 3 per minute                       |
| Browser timeout                                                                 | 60 seconds [2](#user-content-fn-2) |
| Total requests (REST API only) [3](#user-content-fn-3)                          | 6 per minute (1 every 10 seconds)  |

### `/crawl` endpoint limits

The [REST API /crawl endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/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-rendering/pricing/))       |
| Concurrent browsers per account (Workers Bindings only) [1](#user-content-fn-1) | 30 per account ([See pricing](https://developers.cloudflare.com/browser-rendering/pricing/)) |
| New browser instances per minute (Workers Bindings only)                        | 30 per minute                                                                                |
| Browser timeout                                                                 | 60 seconds [2](#user-content-fn-2)                                                           |
| Total requests per min (REST API only) [3](#user-content-fn-3)                  | 600 per minute (10 per second)                                                               |

## FAQ

### How can I manage concurrency and session isolation with Browser Rendering?

If you are hitting concurrency [limits](https://developers.cloudflare.com/browser-rendering/limits/#workers-paid), or want to optimize concurrent browser usage with the [Workers Binding method](https://developers.cloudflare.com/browser-rendering/workers-bindings/), 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-rendering/workers-bindings/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-rendering/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 Rendering closes sessions after one minute of inactivity to prevent unintended usage. You can [increase this inactivity timeout](https://developers.cloudflare.com/browser-rendering/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 Rendering 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-rendering/puppeteer/#list-recent-sessions) or [playwright.history()](https://developers.cloudflare.com/browser-rendering/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 Rendering** page:

[ Go to **Browser Rendering** ](https://dash.cloudflare.com/?to=/:account/workers/browser-rendering) 

Refer to [Browser close reasons](https://developers.cloudflare.com/browser-rendering/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 Rendering 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.

* [ REST API ](#tab-panel-3246)
* [ Workers Bindings ](#tab-panel-3247)

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 */);


}


```

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);

  }

}


```

### 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 Rendering 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-rendering/puppeteer/#keep-alive). Call `browser.close()` to release the browser instance immediately. [↩](#user-content-fnref-2) [↩2](#user-content-fnref-2-2)
3. Enforced with a fixed per-second fill rate, not as a burst allowance. This means you cannot send all your requests at once. The API expects them to be spread evenly over the minute. If you exceed the limit, 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/limits/","name":"Limits"}}]}
```

---

---
title: Pricing
description: There are two ways to use Browser Rendering. Depending on the method you use, here is how billing works:
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-rendering/pricing.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Pricing

 Available on Free and Paid plans 

There are two ways to use Browser Rendering. Depending on the method you use, here is how billing works:

* [**REST API**](https://developers.cloudflare.com/browser-rendering/rest-api/): Charged for browser hours only
* [**Workers Bindings**](https://developers.cloudflare.com/browser-rendering/workers-bindings/): Charged for both browser hours and concurrent browsers

Browser hours are shared across both methods (REST API and Workers Bindings).

| Workers Free                                | Workers Paid       |                                                                                                                           |
| ------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| Browser hours                               | 10 minutes per day | 10 hours per month, then $0.09 per additional hour                                                                        |
| Concurrent browsers (Workers Bindings 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: REST API pricing

If a Workers Paid user uses the REST API 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: Workers Bindings pricing

If a Workers Paid plan user uses the Workers Bindings method 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 Rendering costs?

You can monitor Browser Rendering usage in two ways:

* To monitor your Browser Rendering usage in the Cloudflare dashboard, go to the **Browser Rendering** page.  
[ Go to **Browser Rendering** ](https://dash.cloudflare.com/?to=/:account/workers/browser-rendering)
* The `X-Browser-Ms-Used` header, which is returned in every REST API 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 request to the Browser Rendering REST API 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/pricing/","name":"Pricing"}}]}
```

---

---
title: Changelog
description: Review recent changes to Worker 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-rendering/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 Rendering. For a higher-level summary of major updates to every Cloudflare product, including Browser Rendering, visit [developers.cloudflare.com/changelog](https://developers.cloudflare.com/changelog/).

[ Subscribe to RSS ](https://developers.cloudflare.com/browser-rendering/changelog/index.xml)

## 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-rendering/rest-api/) and [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/) versus the [crawl endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/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-rendering/reference/automatic-request-headers/#bot-detection).

## 2026-03-10

**New REST API endpoint: /crawl (Beta)**
* Added the [/crawl endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/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-rendering/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-rendering/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-rendering/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-rendering/rest-api/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-rendering/faq/#how-do-i-allowlist-browser-rendering) 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-rendering/rest-api/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-rendering/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-rendering/reference/supported-fonts/) guide with instructions on using your own custom fonts via `addStyleTag()` in [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/) or [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/).

## 2025-09-25

**Updates to Playwright, new support for Stagehand, and increased limits**
* [Playwright](https://developers.cloudflare.com/browser-rendering/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-rendering/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-rendering/limits/#workers-paid) for paid plans on both the [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) and [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/).

## 2025-09-22

**Added \`excludeExternalLinks\` parameter to \`/links\` REST endpoint**
* Added `excludeExternalLinks` parameter when using the [/links endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/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-rendering/pricing/) for full details. You can monitor usage via the [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/workers/browser-rendering).

## 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-rendering/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-rendering/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-rendering/playwright/) when using Browser Rendering. Upgrade to the latest version of wrangler to get started.
* The [/content endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/content-endpoint/) now returns the page's title, making it easier to identify pages.
* The [/json endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/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-rendering/rest-api/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-rendering/limits/) charged according to the new [pricing rates](https://developers.cloudflare.com/browser-rendering/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-rendering/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-rendering/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-rendering/rest-api/) is Generally Available.
* Released new endpoints [/json](https://developers.cloudflare.com/browser-rendering/rest-api/json-endpoint/), [/links](https://developers.cloudflare.com/browser-rendering/rest-api/links-endpoint/), and [/markdown](https://developers.cloudflare.com/browser-rendering/rest-api/markdown-endpoint/).

## 2025-04-04

**Playwright support**
* You can now use [Playwright's](https://developers.cloudflare.com/browser-rendering/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-rendering/rest-api/) 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/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-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/mcp-server/","name":"MCP server"}}]}
```

---

---
title: Custom fonts
description: Learn how to add custom fonts to Browser Rendering 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-rendering/features/custom-fonts.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Custom fonts

Browser Rendering uses a managed Chromium environment that includes a [standard set of pre-installed fonts](https://developers.cloudflare.com/browser-rendering/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 Rendering:

* If you are using [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/) with [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/) or [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/), refer to the [Workers Bindings](#workers-bindings) section.
* If you are using the [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/), refer to the [REST API](#rest-api) section.

## Workers Bindings

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.

### From a CDN URL

* [  JavaScript ](#tab-panel-3232)
* [  TypeScript ](#tab-panel-3233)

Example with [Puppeteer](https://developers.cloudflare.com/browser-rendering/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;

    }

  `

});


```

Example with [Puppeteer](https://developers.cloudflare.com/browser-rendering/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;

    }

  `

});


```

### Base64-encoded

The following examples use [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/), but this method works the same way with [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/).

* [  JavaScript ](#tab-panel-3234)
* [  TypeScript ](#tab-panel-3235)

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;

    }

  `

});


```

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;

    }

  `

});


```

## REST API

When using the [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/), 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-rendering/rest-api/screenshot-endpoint/) and [PDF](https://developers.cloudflare.com/browser-rendering/rest-api/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"


```

### 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"


```

For more details on using `addStyleTag` with the REST API, refer to [Customize CSS and embed custom JavaScript](https://developers.cloudflare.com/browser-rendering/rest-api/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/features/","name":"Features"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/features/custom-fonts/","name":"Custom fonts"}}]}
```

---

---
title: Use browser rendering 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 Rendering 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-rendering/how-to/ai.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Use browser rendering with AI

The ability to browse websites can be crucial when building workflows with AI. Here, we provide an example where we use Browser Rendering 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 Rendering 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 Rendering binding to your new Wrangler configuration:

* [  wrangler.jsonc ](#tab-panel-3240)
* [  wrangler.toml ](#tab-panel-3241)

```

{

  "compatibility_flags": [

    "nodejs_compat"

  ]

}


```

```

compatibility_flags = [ "nodejs_compat" ]


```

* [  wrangler.jsonc ](#tab-panel-3242)
* [  wrangler.toml ](#tab-panel-3243)

```

{

  "browser": {

    "binding": "MY_BROWSER"

  }

}


```

```

[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 Rendering

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>;


```

## 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}`)

  }

}


```

If you want to use Browser Rendering 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}`)

  }

}


```

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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/how-to/ai/","name":"Use browser rendering with AI"}}]}
```

---

---
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 Rendering 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-rendering/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 Rendering 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 Rendering to screenshot that page as a PNG.
3. Serve the generated images to social media crawlers.

## Prerequisites

* A Cloudflare account with [Browser Rendering enabled](https://developers.cloudflare.com/browser-rendering/get-started/#rest-api)
* 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>


```

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:

```

---

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 the Cloudflare Browser Rendering REST API.

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 Rendering REST API

 */

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();


```

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 Rendering has [rate limits](https://developers.cloudflare.com/browser-rendering/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>


```

## 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>


```

### 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 Rendering. 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 Rendering documentation](https://developers.cloudflare.com/browser-rendering/)
* [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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/how-to/og-images-astro/","name":"Generate OG images for Astro sites"}}]}
```

---

---
title: Generate PDFs Using HTML and CSS
description: As seen in this Workers bindings guide, Browser Rendering 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-rendering/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 [this Workers bindings guide](https://developers.cloudflare.com/browser-rendering/workers-bindings/screenshots/), Browser Rendering 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 Rendering in two ways:

* **[REST API](https://developers.cloudflare.com/browser-rendering/rest-api/)**: Use the the [/pdf endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/pdf-endpoint/). This is ideal if you do not need to customize rendering behavior.
* **[Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/)**: Use [Puppeteer](https://developers.cloudflare.com/browser-rendering/puppeteer/) or [Playwright](https://developers.cloudflare.com/browser-rendering/playwright/) with Workers Bindings 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-rendering/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 Rendering 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 Rendering binding to your new Wrangler configuration:

* [  wrangler.jsonc ](#tab-panel-3244)
* [  wrangler.toml ](#tab-panel-3245)

```

{

  "browser": {

    "binding": "BROWSER",

  },

}


```

```

[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();

  },

};


```

## 1\. Define HTML and CSS

Rather than using Browser Rendering to navigate to a user-provided URL, manually generate a webpage, then provide that webpage to the Browser Rendering 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>

`;

};


```

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 Rendering 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",

      },

    });

  },

};


```

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 Rendering 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-rendering/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/how-to/pdf-generation/","name":"Generate PDFs Using HTML and CSS"}}]}
```

---

---
title: Build a web crawler with Queues and 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-rendering/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 Rendering

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/how-to/queues/","name":"Build a web crawler with Queues and Browser Rendering"}}]}
```

---

---
title: Automatic request headers
description: Cloudflare automatically attaches headers to every request made through Browser Rendering. 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-rendering/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 Rendering. 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 Rendering:

| Method                                                                                         | Default User-Agent                                                                                              | Customizable                                |
| ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/)                      | 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-rendering/rest-api/crawl-endpoint/) | CloudflareBrowserRenderingCrawler/1.0                                                                           | No                                          |
| [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/)      | 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 Rendering updates its underlying browser engine, destination servers should use the non-configurable headers below to identify Browser Rendering 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 Rendering request when using the [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/)                            |
| cf-brapi-devtools             | A unique identifier for the Browser Rendering request when using [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/)                |
| 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 Rendering, 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 Rendering and has not been tampered with in transit.

### Bot detection

Browser Rendering uses different bot detection IDs depending on the method. The [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) (excluding the [crawl endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/crawl-endpoint/)) and [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/) share one ID, while the crawl endpoint has its own.

| Method                                                                                                                                                                  | Bot detection ID |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) and [Workers Bindings](https://developers.cloudflare.com/browser-rendering/workers-bindings/) | 119853733        |
| [Crawl endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/crawl-endpoint/)                                                                          | 128292352        |

If you are attempting to scan your own zone and want Browser Rendering to access your website freely without your bot protection configuration interfering, you can create a WAF skip rule to [allowlist Browser Rendering](https://developers.cloudflare.com/browser-rendering/faq/#can-i-allowlist-browser-rendering-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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/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-rendering/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 Rendering** page.  
[ Go to **Browser Rendering** ](https://dash.cloudflare.com/?to=/:account/workers/browser-rendering)
2. Select the **Logs** tab.

Browser Rendering sessions are billed based on [usage](https://developers.cloudflare.com/browser-rendering/pricing/). We do not charge for sessions that error due to underlying Browser Rendering 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/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 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-rendering/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 Rendering.

## Identifying Browser Rendering requests

Requests can be identified by the [automatic headers](https://developers.cloudflare.com/browser-rendering/reference/automatic-request-headers/) that Cloudflare attaches:

* [User-Agent](https://developers.cloudflare.com/browser-rendering/reference/automatic-request-headers/#user-agent) — Each Browser Rendering method has a different default User-Agent, which you can use to write targeted `robots.txt` rules
* `cf-brapi-request-id` — Unique identifier for REST API requests
* `Signature-agent` — Pointer to Cloudflare's bot verification keys

To allow or block Browser Rendering traffic using WAF rules instead of `robots.txt`, use the [bot detection IDs](https://developers.cloudflare.com/browser-rendering/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 Rendering (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 Rendering.

### Block only the /crawl endpoint

The [/crawl endpoint](https://developers.cloudflare.com/browser-rendering/rest-api/crawl-endpoint/) identifies itself with the User-Agent `CloudflareBrowserRenderingCrawler/1.0`. To block the `/crawl` endpoint while allowing all other traffic (including other Browser Rendering [REST API](https://developers.cloudflare.com/browser-rendering/rest-api/) endpoints, which use a [different User-Agent](https://developers.cloudflare.com/browser-rendering/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-rendering/rest-api/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>


```

| 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 Rendering 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>


```

### Caching headers

Browser Rendering 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 Rendering be detected by Bot Management?](https://developers.cloudflare.com/browser-rendering/faq/#will-browser-rendering-be-detected-by-bot-management) — How Browser Rendering interacts with bot protection and how to create a WAF skip rule
* [Automatic request headers](https://developers.cloudflare.com/browser-rendering/reference/automatic-request-headers/) — User-Agent strings and non-configurable headers used by Browser Rendering

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/reference/robots-txt/","name":"robots.txt and sitemaps"}}]}
```

---

---
title: Supported fonts
description: Browser Rendering 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-rendering/reference/supported-fonts.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Supported fonts

Browser Rendering 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-rendering/features/custom-fonts/).

## Pre-installed fonts

The following sections list the fonts available in the Browser Rendering 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 Rendering 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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/reference/supported-fonts/","name":"Supported fonts"}}]}
```

---

---
title: REST API timeouts
description: Browser Rendering 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-rendering/reference/timeouts.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# REST API timeouts

Browser Rendering 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-rendering/rest-api/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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/reference/timeouts/","name":"REST API timeouts"}}]}
```

---

---
title: Wrangler
description: Use Wrangler, a command-line tool, to deploy projects using Cloudflare's Workers Browser Rendering 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-rendering/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 Rendering 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 Rendering 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-3256)
* [  wrangler.toml ](#tab-panel-3257)

```

{

  "$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"

  }

}


```

```

"$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.

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-rendering/","name":"Browser Rendering"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-rendering/reference/","name":"Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-rendering/reference/wrangler/","name":"Wrangler"}}]}
```
