> For the complete documentation index, see [llms.txt](https://developers.oxylabs.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.oxylabs.io/products/headless-browser.md).

# Headless Browser

Headless Browser allows you to run and control remote instances for browser-based automation, testing, and web scraping without managing them locally. It provides built-in adaptive security, automatic CAPTCHA handling, geotargeting, integrated residential proxies, session recording, sticky sessions, and persistent profiles.

## Supported libraries

Headless Browser works with any library that supports the **Chrome DevTools Protocol (CDP)**, including:

* [Playwright](https://playwright.dev/) (Python and Node.js)
* [Puppeteer](https://pptr.dev/) (Node.js)
* Other CDP-compatible automation frameworks

## Connection details

<table data-header-hidden><thead><tr><th width="160.5">Field</th><th>Description</th></tr></thead><tbody><tr><td><strong>Protocol</strong></td><td><code>wss://</code> (WebSocket Secure)</td></tr><tr><td><strong>Host (Chromium)</strong></td><td><code>ubc.oxylabs.io</code></td></tr><tr><td><strong>Authentication</strong></td><td>In the URL, user info – <code>wss://USERNAME:PASSWORD@host</code>. Must include username suffix token (e.g. <code>user_ab12</code>). Header-based authentication not supported.</td></tr><tr><td><strong>Transport</strong></td><td>CDP – <code>chromium.connectOverCDP</code> (Playwright) / <code>puppeteer.connect</code> (Puppeteer)</td></tr><tr><td><strong>Domains</strong></td><td><code>oxylabs.io</code> – connection endpoints you authenticate and connect to (e.g. <a href="http://ubc.oxylabs.io">ubc.oxylabs.io</a>)<br><code>headlesify.io</code> – dashboard, session inspection and recordings (e.g. <a href="http://dashboard.headlesify.io">dashboard.headlesify.io</a>, <a href="http://vnc.headlesify.io">vnc.headlesify.io</a>).</td></tr><tr><td><strong>Rate limits</strong></td><td><code>100</code> concurrent sessions, <code>10</code> session per second. <a href="#need-a-feature-enabled-1">See more</a>.</td></tr></tbody></table>

## Features

Oxylabs Headless Browser includes built-in, cloud-native features designed to be used through query parameters to your WebSocket connection URL.

<table data-header-hidden><thead><tr><th width="250"></th><th></th></tr></thead><tbody><tr><td><a href="/pages/eOLS2KWOcJM75AbXrc48"><strong>CAPTCHA Handling</strong></a> </td><td>Automatic, real-time CAPTCHA handling and monitoring.</td></tr><tr><td><a href="/pages/ZdKDWxjZn3NUMqeSaIzt"><strong>Proxy &#x26; Geolocation Targeting</strong></a></td><td>Route sessions through specific countries, states, or cities.</td></tr><tr><td><a href="/pages/FupqktgKuk6zu1GiCrnf"><strong>Device Emulation</strong></a></td><td>Emulate device-specific fingerprints and viewports.</td></tr><tr><td><a href="/pages/KLRkghHiIaZV6SP9XkuX#session-inspection-vnc"><strong>Session Inspection (VNC)</strong></a></td><td>Monitor live headless browser sessions.</td></tr><tr><td><a href="/pages/KLRkghHiIaZV6SP9XkuX#session-recording"><strong>Session Recording</strong></a> </td><td>Record browser sessions in video format.</td></tr><tr><td><a href="/pages/8d50WvdPnEEhZnaxBLSM#persistent-sessions"><strong>Persistent Sessions</strong></a></td><td>Create and manage sticky browser instances.</td></tr><tr><td><a href="/pages/8d50WvdPnEEhZnaxBLSM#persistent-profiles"><strong>Persistent Profiles</strong></a></td><td>Save and restore cookies/localStorage across sessions. <em>(</em><a href="#need-a-feature-enabled-1"><em>Activation required</em></a><em>)</em></td></tr></tbody></table>

### Passing parameters

All features are enabled and configured by appending query parameters directly to your WebSocket Secure endpoint by chaining multiple features together using ampersands (`&`).

```bash
# Example: Connecting to Chrome with US Geolocation, CAPTCHA handling, and Live VNC Stream
wss://USER:PASS@ubc.oxylabs.io?p_cc=US&solve_captcha=true&o_vnc=true
```

## Code examples

Below are basic examples to initialize a cloud-hosted browser session:

{% tabs %}
{% tab title="Python (Playwrigth)" %}

```python
from playwright.sync_api import sync_playwright

username = "USERNAME" # include any account suffix
password = "PASSWORD"
endpoint = "ubc.oxylabs.io"
browser_url = f"wss://{username}:{password}@{endpoint}?p_cc=US"

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(browser_url)
    page = browser.new_page()
    page.goto("https://ip.oxylabs.io/location")
    print(page.title())
    browser.close()
```

{% endtab %}

{% tab title="JavaScript (Playwright)" %}

```javascript
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //For testing only
import { chromium } from "playwright";

const username = "USERNAME";
const password = "PASSWORD";
const endpoint = "ubc.oxylabs.io";
const browserUrl = `wss://${username}:${password}@${endpoint}?p_cc=US`;

(async () => {
    const browser = await chromium.connectOverCDP(browserUrl);
    const ctx = browser.contexts()[0] || (await browser.newContext());
    const page = ctx.pages()[0] || (await ctx.newPage());
    await page.goto("https://ip.oxylabs.io/location");
    console.log(await page.title());
    await browser.close();
})();
```

{% endtab %}

{% tab title="JavaScript (Puppeteer)" %}

```javascript
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //For testing only
import puppeteer from "puppeteer";

const username = "USERNAME";
const password = "PASSWORD";
const endpoint = "ubc.oxylabs.io";
const browserUrl = `wss://${username}:${password}@${endpoint}?p_cc=US`;

(async () => {
    const browser = await puppeteer.connect({ browserWSEndpoint: browserUrl });
    const page = await browser.newPage();
    await page.goto("https://ip.oxylabs.io/location");
    console.log(await page.title());
    await browser.close();
})();
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**Note:** Examples use `USERNAME` and `PASSWORD` for readability only. In real projects, load them from environment variables (e.g. `process.env.OXYLABS_USERNAME` / `process.env.OXYLABS_PASSWORD`) via a `.env` file.
{% endhint %}

{% hint style="warning" %}
**JavaScript:** Use `NODE_TLS_REJECT_UNAUTHORIZED="0"` flag before requiring Playwright in Node.js if you encounter TLS/certificate errors. For local testing only. In production, scope trust to the specific connection or add the provider’s CA certificate to your trust store instead.
{% endhint %}

## Parameter reference <a href="#need-a-feature-enabled" id="need-a-feature-enabled"></a>

<table><thead><tr><th width="200">Parameter</th><th width="431.5">Description</th><th>Type</th></tr></thead><tbody><tr><td><code>p_cc</code></td><td>Country geolocation in <code>ISO 3166-1 alpha-2</code> 2-letter code (e.g., <code>US</code>, <code>DE</code>).</td><td>string</td></tr><tr><td><code>p_state</code></td><td>State geolocation in lowercase (e.g., <code>texas</code>). Overrides <code>p_cc</code> if both are used. <a href="https://content.gitbook.com/content/BQ7Zf9paoN3FTeGcyfY1/blobs/cVjpiu1GKicluiVIT9og/us_states.txt">Supported states list</a>.</td><td>string</td></tr><tr><td><code>p_city</code></td><td>City geolocation, lowercase, <code>_</code> for spaces (e.g., <code>new_york</code>). <code>p_cc</code> / <code>p_state</code> required.</td><td>string</td></tr><tr><td><code>p_device</code></td><td>Set device fingerprints, viewports, and user-agents. Supports <code>desktop</code> (default) and <code>mobile</code>.</td><td>string</td></tr><tr><td><code>solve_captcha</code></td><td>Automatic, real-time CAPTCHA solving on page loads. Default: <code>true</code>.</td><td>boolean</td></tr><tr><td><code>record</code></td><td>Record headless session video. Default: <code>false</code>.</td><td>boolean</td></tr><tr><td><code>record_name</code></td><td>Name the recording for easy lookup (<code>^[a-zA-Z0-9_-]{1,64}$</code>).</td><td>string</td></tr><tr><td><code>session_name</code></td><td>Sticky sessions – reconnects to the same live remote browser (<code>^[A-Za-z0-9-]{3,36}$</code>, supports hyphens, no underscores). Max TTL – 24h.</td><td>string</td></tr><tr><td><code>keep_alive</code></td><td>Closes the remote browser instance on client disconnect when set to <code>false</code>. Default: <code>true</code>.</td><td>boolean</td></tr><tr><td><mark style="background-color:yellow;"><code>o_profile</code></mark></td><td>Persistent profiles – save/restore cookies and <code>localStorage</code> with a named profile (<code>^[A-Za-z0-9_-]{1,36}$</code>).</td><td>string</td></tr><tr><td><mark style="background-color:yellow;"><code>o_profile_save</code></mark></td><td>Persistent profiles – force profile save mid-session. Default: <code>true</code>.</td><td>boolean</td></tr><tr><td><code>proxy_resi_ses_id</code></td><td>Custom session ID to pin residential exit IP across sessions (<code>^[A-Za-z0-9]{3,36}$</code>).</td><td>string</td></tr><tr><td><code>proxy_resi_ses_time</code></td><td>Duration to hold the pinned residential proxy exit IP in minutes. Min <code>1</code>, max <code>1440</code> (24h).</td><td>integer</td></tr></tbody></table>

&#x20;    – available after manual activation for your account. [Learn more](#need-a-feature-enabled-1).

## Recommended setup

### Optimizing traffic

Scraping dynamic pages often causes the browser to download unnecessary assets such as heavy media, tracking scripts, images, and fonts. This consumes bandwidth and slows down execution times.

You can intercept and abort these requests programmatically before they consume resources:

{% tabs %}
{% tab title="Python (Playwright)" %}

```python
# Abort heavy assets to save bandwidth and improve speeds
def block_resources(route):
    if route.request.resource_type in ["image", "stylesheet", "media", "font"]:
        route.abort()
    else:
        route.continue_()

page.route("**/*", block_resources)
```

{% endtab %}

{% tab title="JavaScript (Playwright)" %}

```javascript
// Abort heavy assets to save bandwidth and improve speeds
await page.route("**/*", (route) => {
    const type = route.request().resourceType();
    if (["image", "stylesheet", "media", "font"].includes(type)) {
        return route.abort();
    }
    return route.continue();
});
```

{% endtab %}
{% endtabs %}

### Error handling and retries <a href="#error-handling-and-retries" id="error-handling-and-retries"></a>

Network hiccups and 10-sessions-per-second rate limit mean a single `connectOverCDP` call can fail transiently. Wrap connection in a `retry` with exponential `backoff`, and cap each attempt with a timeout. The example below uses standard Playwright API:

```javascript
const { chromium } = require("playwright");

const MAX_RETRIES = 5;
const BASE_DELAY_MS = 1000;

async function connectWithRetry(endpoint, { maxRetries = MAX_RETRIES } = {}) {
    let lastError;
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            // Cap how long a single connection attempt may take.
            return await chromium.connectOverCDP(endpoint, { timeout: 60_000 });
        } catch (err) {
            lastError = err;
            // Exponential backoff with jitter — backs off when you hit the
            // 10-new-sessions/second rate limit instead of hammering the endpoint.
            const delay = BASE_DELAY_MS * 2 ** attempt + Math.random() * 250;
            console.warn(
                `connect attempt ${attempt + 1} failed: ${err.message}; ` +
                `retrying in ${Math.round(delay)}ms`
            );
            await new Promise((resolve) => setTimeout(resolve, delay));
        }
    }
    throw lastError;
}
```

### Resource cleanup <a href="#resource-cleanup" id="resource-cleanup"></a>

Always close the browser when you are done, even if your automation throws midway. Use a `try { ... } finally { ... }` block so cleanup runs on every path:

```javascript
const browser = await connectWithRetry(endpoint);
try {
    const ctx = browser.contexts()[0] || (await browser.newContext());
    const page = ctx.pages()[0] || (await ctx.newPage());
    await page.goto("https://ip.oxylabs.io/location");
    // ... your automation ...
} finally {
    // Runs even if the block above throws. Without this, a session left open
    // after an exception lingers and counts against your 100 concurrent-session limit.
    await browser.close();
}
```

If an exception occurs between connect and close and you do not clean up, the remote session stays open and consumes one of your **100 concurrent sessions** until it is reclaimed. Leaking sessions under load can exhaust the limit and block new connections.

## Need a feature enabled? <a href="#need-a-feature-enabled" id="need-a-feature-enabled"></a>

Some Headless Browser features are off or restricted by default. To access them, contact Oxylabs support ([live chat](https://oxylabs.io/) or [email](mailto:support@oxylabs.io)) or your Dedicated Account Manager:

* **Persistent profiles** – gain access to the feature and `o_profile` parameters.
* **More profiles** – raise account's `max_profiles` cap for persistent profiles.
* **Restricted targets** – unlock more target categories via KYC process.
* **Higher rate limits** – increase beyond 100 concurrent sessions / 10 per second.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developers.oxylabs.io/products/headless-browser.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
