---
title: "Scrape.ly Browser · Scrape.ly Docs"
description: "Run Playwright and Puppeteer on the hosted anti-bot browser: turn it on per Actor, set its location, and see what it costs."
url: https://scrape.ly/docs/browser
markdown: https://scrape.ly/docs/browser.md
---

# Scrape.ly Browser

> Run Playwright and Puppeteer on the hosted anti-bot browser: turn it on per Actor, set its location, and see what it costs.

The Scrape.ly browser is a hosted Chromium your Actor's browsers can run on instead of inside the run container. It has anti-bot fingerprinting, solves Cloudflare Turnstile challenges, and routes traffic through its own residential proxy.

It's **off by default**. You turn it on per Actor, rebuild, and your Playwright or Puppeteer code connects to it without changes.

## Turn it on

### 1. Enable it for the Actor

In the dashboard, open the Actor and go to **Settings** → **Scrapely Browser**. Turn on **Use Scrapely browser** and confirm. Only the Actor's owner can change it.

Or with the API:

```bash
curl -X PUT "https://api.scrape.ly/v2/actors/<actorId>/browser-patch" \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"browserPatchEnabled": true}'
```

### 2. Rebuild

The setting is applied when the Actor is built, so start a new build: `scrapely push`, **Builds** → **Build**, or the [build API](https://scrape.ly/docs/api-actors.md#start-a-build). Existing builds keep working as they were.

### 3. Run it

Runs of the new build connect every browser launch to the Scrape.ly browser. The build log shows which mode a build uses:

```text
[Scrapely Build] Browser patch: on (remote browser)
```

> [!IMPORTANT]
> The setting follows the **build**, not the Actor. If you change it and don't rebuild, runs still use the old mode. In the dashboard, the Settings card warns you when your latest build doesn't match, and builds that use the Scrape.ly browser carry a **Scrapely browser** badge.

## On and off at a glance

| | Off (default) | On |
| --- | --- | --- |
| Where browsers run | In the run container, from the browsers in your image | On the Scrape.ly browser, over CDP |
| Browser downloads in the build | Kept | Removed: `playwright install`, `pyppeteer-install`, Puppeteer's postinstall download |
| Proxy for browser traffic | Whatever you configure | The Scrape.ly browser's own proxy. Your proxy URLs are ignored for browsers |
| Firefox, WebKit, Camoufox | Launch as requested | Redirected to the Scrape.ly browser (Chromium) |
| Billing | Compute units | Compute units plus [browser credits](#pricing) |
| Build object | `browserPatched: false` | `browserPatched: true` |
| Run environment | `SCRAPELY_BROWSER_PATCH=0` | Not set |

Builds made before this setting existed have `browserPatched: null` and count as **on**. Rebuild with the setting off to move them to local browsers.

## What changes in the build

With the setting on, the build:

- Removes browser binary downloads from the Dockerfile: `playwright install`, `pyppeteer-install`, `PLAYWRIGHT_BROWSERS_PATH` downloads, Puppeteer's postinstall download, and Apify's `check-playwright-version.mjs` guard. The Playwright and Puppeteer packages and OS-package browsers (`apt-get install chromium`) stay.
- Replaces each `playwright install` with `mkdir -p` of the browsers directory (`PLAYWRIGHT_BROWSERS_PATH`, else Playwright's default), so later steps that touch it, such as `chmod -R a+rX /ms-playwright`, still work.
- Makes sure the image loads the redirect. `scrapely/actor-node*` and `scrapely/actor-python*` images already do, through `NODE_OPTIONS` and `sitecustomize`. Other images get a small preload for Node.js or Python and nothing else.
- Never edits your stored source.

It works the same whether the account-level **Transform actors** setting is on or off.

## What gets redirected

On the platform, the SDK patches every launch path so it connects to the browser in `CDP_URL`: Playwright uses `chromium.connectOverCDP()`, Puppeteer uses `puppeteer.connect({ browserWSEndpoint })`. Actors that never import the SDK are covered too, through the image preload.

| Launch path | JavaScript | Python |
| --- | --- | --- |
| Crawlee crawlers | `PlaywrightCrawler`, `PuppeteerCrawler`, other `BrowserCrawler` subclasses | `PlaywrightCrawler` |
| Raw launches | `chromium.launch()`, `puppeteer.launch()` | `chromium.launch()` and `launch_persistent_context()` (sync and async), pyppeteer `launch()` |
| Other browsers | Firefox, WebKit, and Camoufox launchers go to Chromium | Firefox, WebKit, and Camoufox go to Chromium |

## Choose the location and fingerprint

The Scrape.ly browser never takes a proxy URL. Pick where its traffic exits, and how it looks, with a browser configuration:

```js [JavaScript]
import { Actor, PlaywrightCrawler } from 'scrapely';

await Actor.main(async () => {
    const browserConfiguration = await Actor.createBrowserConfiguration({
        countryCode: 'US',
        state: 'California',
        os: 'windows',
    });

    const crawler = new PlaywrightCrawler({
        browserConfiguration,
        async requestHandler({ page }) {
            await Actor.pushData({ title: await page.title() });
        },
    });
    await crawler.run(['https://example.com']);
});
```

```python [Python]
import asyncio

from crawlee.crawlers import PlaywrightCrawler
from scrapely import Actor


async def main():
    browser_configuration = await Actor.create_browser_configuration(
        country_code="US", state="California", os="windows"
    )
    crawler = PlaywrightCrawler(browser_configuration=browser_configuration)
    await crawler.run(["https://example.com"])


asyncio.run(Actor.main(main))
```

| Option (JS / Python) | CDP URL parameter | Format | Default |
| --- | --- | --- | --- |
| `countryCode` / `country_code` | `proxy_country` | ISO 3166-1 alpha-2, e.g. `US` | The run default, `US` |
| `state` | `proxy_state` | Region name, e.g. `California` (sent as `california`) | None |
| `city` | `proxy_city` | City name, e.g. `New York` (sent as `new.york`) | None |
| `os` | `os` | `windows`, `macos`, or `android` | Chosen by the browser |
| `timezone` | `timezone` | IANA name, up to 64 characters | The exit country's zone |
| `language` | `language` | Tags without q-values, e.g. `de-CH,de` | The exit country's language |
| `adblock` | `adblock` | Boolean | `false` |
| `solveCloudflare` / `solve_cloudflare` | `solve_cloudflare` | Boolean | `true` |

- **Input aliases.** Pass your Actor input straight in: `browserCountry`, `browserState`, and `browserCity` fill `countryCode`, `state`, and `city`. Explicit options win.
- **Precedence.** The location comes from the browser configuration, then the country and subdivision of a platform `ProxyConfiguration` on the crawler, then the run default.
- **Validation.** Invalid values throw (JavaScript) or raise `ValueError` (Python) when you create the configuration, before any browser starts.
- **Sessions.** Proxy groups and session IDs don't apply. Each browser connection is a fresh browser session, and the first page that launches a browser fixes its location.
- **Same code everywhere.** The option is accepted locally and in builds with the setting off, and ignored there.

> [!WARNING]
> Custom proxies (`proxyUrls`, `newUrlFunction`, `launch({ proxy })`) are ignored for browser traffic, with a one-time warning. They still work for HTTP crawlers such as `CheerioCrawler`.

## Connect over CDP yourself

Every run gets `CDP_URL`, whatever the setting. Connecting to it directly uses the Scrape.ly browser even in a build with the setting off, and bills browser credits:

```js [JavaScript]
import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP(process.env.CDP_URL);
```

```python [Python]
import os

from playwright.async_api import async_playwright


async def main():
    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(os.environ["CDP_URL"])
```

To set the location on a direct connection, add the CDP URL parameters from the table above to the URL.

- The URL identifies the run. The browser accepts it only while the run is `READY`, `RUNNING`, `ABORTING`, or `TIMING-OUT`, and only if the account running the Actor has a positive balance.
- It carries the run's default exit country as `proxy_country` (`US`).

> [!CAUTION]
> Don't log or store `CDP_URL`. Anyone with it can open browser sessions billed to the run while the run is active.

## Pricing

The Scrape.ly browser is billed in **browser credits**, on top of the run's compute units.

- **Who pays:** the account that runs the Actor. For a Store Actor that's the user, not the developer.
- **What counts:** session time in 30-second blocks (rounded up, at least one block) and the browser's proxy traffic per MB (rounded up, at least 1 MB). That traffic is part of the credits and is never billed again as proxy GB.
- **Price:** your price per credit is under **Billing** → **Pricing**.
- **Spending cap:** browser credits count toward the run's `maxTotalChargeUsd`. When the cap is reached, the run is stopped.

Where to see it:

| Place | Shows |
| --- | --- |
| Run page → cost breakdown | **Browser credits** for the run |
| **Billing** → **Current period** | **Browser credits**, with the session time and proxy traffic they include |
| Run object in the API | `usage.BROWSER_CREDITS`, `usage.BROWSER_SECONDS`, `usageUsd.BROWSER_CREDITS` |
| `GET /v2/users/me/usage/monthly` | `BROWSER_CREDITS` in `monthlyServiceUsage` |

## Local runs

- `scrapely run` and other local runs have no `CDP_URL`, so browsers launch on your machine, as in plain Crawlee. The browser configuration is ignored.
- `setCdpUrl(url)` (JavaScript) or `set_cdp_url(url)` (Python) points the SDK at a CDP endpoint you run yourself. The Scrape.ly browser only accepts connections from platform runs.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| Browsers still launch in the container | The build was made before you turned the setting on | Rebuild, then check the build log or the **Scrapely browser** badge |
| `cdpUrl is required. Local browser launch is disabled` | A build with the setting on started a browser without `CDP_URL` | Don't unset `CDP_URL` or clear it with `setCdpUrl(undefined)` in code that runs on the platform |
| `Custom proxies are ignored when using the Scrapely remote browser` | A browser crawler or launch got your own proxy | Use a browser configuration; keep your proxy for HTTP crawlers |
| `Subdivision "XX-YY" has no Scrapely browser mapping` | A proxy `subdivisionCode` the browser can't map | Set `state` in the browser configuration |
| `Unknown option browserConfiguration` | It was passed to an HTTP crawler | Only browser crawlers accept it |
| Browser fails to launch after turning the setting off | The image has no browser binaries | Use a browser image such as `scrapely/actor-node-playwright-chrome` or install the browsers in your Dockerfile |

Related: [Browser patch API](https://scrape.ly/docs/api-actors.md#browser-patch), [JavaScript environment](https://scrape.ly/docs/sdk-node-environment.md#browsers-and-remote-cdp), [Python environment](https://scrape.ly/docs/sdk-python-environment.md#remote-browser), [Proxy](https://scrape.ly/docs/proxy.md).

---

- Previous: [Proxy](https://scrape.ly/docs/proxy.md)
- Next: [LLM gateway](https://scrape.ly/docs/llm-gateway.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
