This is the full developer documentation for BrowserThing # Overview > A self-hosted browser pool for Playwright applications. Reuse running browsers for short tasks, with session cleanup and configurable browser recycling. **Use Playwright in your app. Run browsers elsewhere.** BrowserThing is a self-hosted browser pool for applications using Playwright. It was built for workloads with many short browser tasks. Starting a new browser for every task adds time and CPU use. Keeping the same browser running indefinitely can let memory use grow or leave it in a bad state. BrowserThing reuses running browsers and replaces them after a configurable number of sessions. Your application runs normal Playwright code and connects through one WebSocket endpoint. BrowserThing selects an available worker and cleans up each session when its connection closes. Browser capacity can grow without changes to the endpoint your applications use. You deploy and update the server, PostgreSQL, and browser workers. Run workers on separate machines to keep browser CPU and memory use off your application servers. Add workers when you need more browser capacity. The pool is built for your own applications and trusted clients. Browser contexts separate cookies and storage, but sessions share a browser process on each worker. See the [security boundary](/browserthing/docs/security/). These guides describe **BrowserThing v0.6.0**, with **Playwright 1.63.0** workers. ```text Your application: Playwright code | v BrowserThing server ---- PostgreSQL | +---- Chromium workers +---- Firefox workers +---- WebKit workers ``` ## Connect your application [Section titled “Connect your application”](#connect-your-application) Follow the [quick start](/browserthing/docs/quick-start/) to run the service locally with Docker Compose and connect your Playwright code. It uses a screenshot as an example browser task. Then see the [connection examples](/browserthing/docs/connect/) for Node.js, Python, and other browser types. Keep the steps of each browser task in your application. Several applications can share one internal browser endpoint, with browser capacity managed in one place. ## Compare performance [Section titled “Compare performance”](#compare-performance) See [benchmark results](/browserthing/docs/benchmarks/) for task time and CPU use in a comparison with Browserless on the same hardware. ## Run your browser service [Section titled “Run your browser service”](#run-your-browser-service) * [Architecture](/browserthing/docs/architecture/) explains sessions, worker selection, and recycling. * [Deployment](/browserthing/docs/deployment/) covers API keys, networking, and persistent data. * [Scaling](/browserthing/docs/scaling/) covers workers on one host or several hosts. * [Security](/browserthing/docs/security/) explains the trust and isolation boundaries. ## Find a setting or endpoint [Section titled “Find a setting or endpoint”](#find-a-setting-or-endpoint) Use the [configuration reference](/browserthing/docs/configuration/) and [API guide](/browserthing/docs/api/). For a failed connection, start with [troubleshooting](/browserthing/docs/troubleshooting/). ## Upgrade an existing installation [Section titled “Upgrade an existing installation”](#upgrade-an-existing-installation) Follow the [upgrade guide](/browserthing/docs/upgrading/) to update the server and worker images and preserve your database volume. ## Read docs as Markdown [Section titled “Read docs as Markdown”](#read-docs-as-markdown) Use **Copy Markdown** or **View Markdown** on any docs page. The [docs index](/browserthing/llms.txt) links to the [complete documentation](/browserthing/llms-full.txt) as text. These files update with the site. The project uses the [Apache-2.0 license](https://github.com/mbroton/browserthing/blob/main/LICENSE). Report problems or suggest changes in [GitHub issues](https://github.com/mbroton/browserthing/issues). # API reference > Inspect capacity and workers, create sessions, and attach Playwright clients by session ID. BrowserThing provides a REST control plane and WebSocket connections for Playwright. The examples use a local server. Add `-H "Authorization: Bearer $BROWSERTHING_API_KEY"` to REST requests after you enable authentication. The full request and response schemas are in the [v0.6.0 OpenAPI document](https://github.com/mbroton/browserthing/blob/v0.6.0/server/openapi.yaml). WebSocket routes are documented separately below. ## Capacity and workers [Section titled “Capacity and workers”](#capacity-and-workers) | Method and path | Purpose | | ------------------ | --------------------------------------------------- | | `GET /v1/capacity` | Read browser capacity, free slots, and queue depth. | | `GET /v1/workers` | List worker records. | | `GET /healthz` | Check process health without authentication. | | `GET /readyz` | Check database readiness without authentication. | ```sh curl -fsS http://localhost:8080/v1/capacity curl -fsS http://localhost:8080/v1/workers ``` For one idle Chromium worker with five slots, `GET /v1/capacity` returns `200 OK` with these values: ```json { "browsers": [ { "browser": "chromium", "workers": 1, "max_slots": 5, "active_sessions": 0, "available_slots": 5 } ], "totals": { "workers": 1, "max_slots": 5, "active_sessions": 0, "available_slots": 5 }, "queued": 0, "max_queue_size": 100 } ``` The `queued` capacity value belongs to the server replica that handles the request. It is not a sum across replicas. ## Create and attach a session [Section titled “Create and attach a session”](#create-and-attach-a-session) `POST /v1/sessions` reserves a session. Supply the browser type and the client’s Playwright version so the server can select a matching worker: ```sh curl -fsS http://localhost:8080/v1/sessions \ -H 'Content-Type: application/json' \ -d '{"browser":"chromium","playwright_version":"1.63.0"}' ``` Success returns `201 Created` and a session object. These are selected fields from an example response; IDs differ for each session: ```json { "id": "6fb174a2-4dc3-4c29-a33c-20f3b05da4bd", "browser": "chromium", "playwright_version": "1.63.0", "mode": "default", "status": "pending", "started_at": null, "connect_metadata": {} } ``` Attach before the pending session expires (30 seconds by default). This complete example creates the session and uses its returned ID immediately. It needs Node.js 20 or later and the matching client: ```sh npm install playwright@1.63.0 ``` Save this as `create-session.mjs`. Set `BROWSERTHING_URL` for a remote server and `BROWSERTHING_API_KEY` if authentication is enabled. The key is used for both the REST request and the WebSocket connection. ```js import { chromium } from 'playwright'; const server = process.env.BROWSERTHING_URL || 'http://localhost:8080'; const key = process.env.BROWSERTHING_API_KEY; const headers = key ? { Authorization: `Bearer ${key}` } : {}; const response = await fetch(new URL('/v1/sessions', server), { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ browser: 'chromium', playwright_version: '1.63.0' }), }); if (!response.ok) { throw new Error(`Create session: HTTP ${response.status}: ${await response.text()}`); } const session = await response.json(); const endpoint = new URL(`/sessions/${session.id}`, server); endpoint.protocol = endpoint.protocol === 'https:' ? 'wss:' : 'ws:'; const browser = await chromium.connect(endpoint.href, { headers }); try { const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: 'preview.png', fullPage: true }); } finally { await browser.close(); } ``` Run `node create-session.mjs`. It saves `preview.png` on the client machine. This attaches to a pending session. It does not resume a session after its connection has closed. The connecting client’s major and minor version must match the session’s worker. ## Inspect or terminate a session [Section titled “Inspect or terminate a session”](#inspect-or-terminate-a-session) | Method and path | Purpose | | -------------------------- | -------------------------------------- | | `POST /v1/sessions` | Create a pending session. | | `GET /v1/sessions/{id}` | Read one session record. | | `DELETE /v1/sessions/{id}` | Complete a pending or running session. | ```sh curl -fsS http://localhost:8080/v1/sessions/SESSION_ID curl -fsS -X DELETE http://localhost:8080/v1/sessions/SESSION_ID ``` `GET` returns `200 OK` with the session object. A successful `DELETE` returns `204 No Content` with no response body. An unknown ID returns `404 Not Found`. For a running session, the relay notices deletion on its next heartbeat and closes both WebSocket peers with code `1001`. The normal delay is at most one `SESSION_HEARTBEAT_INTERVAL` (10 seconds by default). ## WebSocket routes [Section titled “WebSocket routes”](#websocket-routes) ```text GET /?browser=chromium -> create session -> select worker -> relay Playwright traffic GET /sessions/{id} -> attach to pending session -> relay Playwright traffic ``` Both routes accept `Authorization: Bearer pwd_...` or `?token=pwd_...`. The first route uses `DEFAULT_BROWSER_TYPE` when `browser` is omitted. Supported browser types are `chromium`, `firefox`, and `webkit`. The relay forwards `User-Agent` and `x-playwright-*` headers to workers, plus its own `x-pwd-session-id`. It does not forward client authorization, cookies, or query tokens to the worker. ## Error responses [Section titled “Error responses”](#error-responses) REST errors include an HTTP status and a JSON body with details. For example, requesting `mode: dedicated` returns `422 Unprocessable Entity` with these fields: ```json { "title": "Unprocessable Entity", "status": 422, "detail": "dedicated mode is not available yet" } ``` Use `curl -sS -i` to see both the status and error body. `curl -f` hides the body on errors. Failed WebSocket connections return their error before the upgrade; their JSON bodies contain a `message` field. See [HTTP errors](/browserthing/docs/troubleshooting/#http-errors) for causes and next actions. ## API keys [Section titled “API keys”](#api-keys) Manage keys with the server CLI: ```sh docker compose exec server server apikey create --name client docker compose exec server server apikey list docker compose exec server server apikey revoke --id KEY_ID ``` All valid keys have full access in v0.6.0. Worker registration and lifecycle routes under `/internal/workers` are for the worker protocol; their schemas are also included in OpenAPI. # Architecture > How BrowserThing routes sessions, keeps browsers warm, and recovers worker capacity. Your application runs the Playwright code that controls browser tasks. BrowserThing runs the browsers and manages their sessions through a Go server, a PostgreSQL database, and TypeScript Playwright workers. Each worker keeps one browser process running between tasks. ```text Applications using Playwright | | WebSocket v Server <----------> PostgreSQL | | WebSocket relay +------------+------------+ v v v Worker Worker Worker Chromium Firefox WebKit | | | contexts contexts contexts Workers -> Server: HTTP registration and heartbeats ``` ## The server [Section titled “The server”](#the-server) The server authenticates clients, selects a worker, and relays WebSocket messages. It also provides the REST API for sessions, workers, and capacity. PostgreSQL stores worker records, sessions, and API-key hashes. Browser traffic passes through the server for the whole session. A server outage ends connections that use that relay. ## The workers [Section titled “The workers”](#the-workers) Each worker registers its address, browser type, Playwright version, and slot limit with the server. It sends regular heartbeats to report that it is alive. Workers serve up to `MAX_SLOTS` sessions at once. The default is five. Sessions have separate browser contexts, but share the worker’s browser process. ## A connection [Section titled “A connection”](#a-connection) 1. The server checks the client’s key, if authentication is enabled. 2. It claims capacity on a matching worker and creates a session record. 3. It connects to the worker and starts the WebSocket relay. 4. The client creates contexts and pages through Playwright. 5. When the connection closes, the worker cleans up the session’s resources. When all matching slots are busy, requests can wait in the server’s admission queue. Each server replica has its own queue. See [queue settings](/browserthing/docs/configuration/#server). ## Worker recycling and failures [Section titled “Worker recycling and failures”](#worker-recycling-and-failures) By default, a worker drains after 50 lifetime session claims. Draining stops new sessions and waits for existing sessions for up to `DRAIN_TIMEOUT`. The worker then replaces its browser process and resumes service without restarting the container. A shutdown signal such as `SIGTERM` drains and stops the worker instead. When the drain timeout expires, remaining sessions are closed. The supplied Compose file sets this timeout to **30 seconds**; the worker’s standalone default is 300 seconds. Set it for the time your tasks need to finish. See the [worker settings](/browserthing/docs/configuration/#worker). Selection concentrates load on longer-serving workers to stagger recycling. Dead workers lose their sessions, and the server closes out their records so capacity can recover. If a browser crashes, all sessions on that worker end. BrowserThing restores capacity; your client code must decide whether to retry its work. Sessions separate cookies and storage but share a browser process on each worker. See the [security boundary](/browserthing/docs/security/) for client trust and network access requirements. # Benchmarks > Compare task time and CPU use for BrowserThing and Browserless. BrowserThing keeps browsers running between sessions. This benchmark opened and closed a Playwright connection for each task. In this setup, Browserless 2.56.0 [launched a new browser process](https://github.com/browserless/browserless/blob/v2.56.0/src/browsers/browsers.playwright.ts#L197-L209) for each connection. ```text BrowserThing: Connect -> Use running browser -> Open page -> Read Browserless: Connect -> Start new browser -> Open page -> Read ``` Browserless also supports [session reuse](https://docs.browserless.io/baas/session-management), which this benchmark did not use. ## Results [Section titled “Results”](#results) The comparison used an AWS `m8i.xlarge` with 4 vCPUs and 16 GB of memory. Both systems used the same Playwright version, with one BrowserThing worker and one Browserless node. | Measurement | BrowserThing | Browserless | | ----------------------------------- | ------------ | ----------- | | Get a browser, open a page, read it | **51 ms** | 217 ms | | CPU time per task | **0.09 s** | 0.70 s | | 1,000 such tasks, 5 at a time | **26 s** | 154 s | These results measure a short page-read task. Results for your application depend on the pages, hardware, and number of concurrent sessions. ## Why browser reuse helps [Section titled “Why browser reuse helps”](#why-browser-reuse-helps) BrowserThing avoids the browser launch cost on each connection. This saves time and CPU when a service opens and closes many short sessions. For application tasks, browser reuse reduces session startup overhead. Page loading and browser actions still take time. Measure the complete task with your own pages before estimating the benefit for your application. Each session uses separate browser contexts for cookies, storage, and cache. Sessions on the same worker share a browser process. Browser launch options apply to the whole worker, and a browser crash ends all sessions on that worker. See [architecture](/browserthing/docs/architecture/) and the [security boundary](/browserthing/docs/security/) for details. ## Measure your workload [Section titled “Measure your workload”](#measure-your-workload) The repository includes [benchmark scripts](https://github.com/mbroton/browserthing/tree/v0.6.0/scripts/bench) for time to first page and session throughput. Use the [scaling guide](/browserthing/docs/scaling/#tune-slots) to tune worker capacity for your workload. # Configuration > Server and worker environment variables for BrowserThing v0.6.0. Configure the server and workers with environment variables. These are the application defaults for v0.6.0. A Compose file can override them. ## Server [Section titled “Server”](#server) Server duration values use Go duration notation, such as `10s` or `5m`. | Variable | Default | Purpose | | ---------------------------- | ---------- | --------------------------------------------------------------------------- | | `DATABASE_URL` | Required | PostgreSQL connection string. | | `LISTEN_ADDR` | `:8080` | HTTP and WebSocket listen address. | | `DEFAULT_BROWSER_TYPE` | `chromium` | Browser selected when a connection omits `?browser=`. | | `MAX_QUEUE_SIZE` | `100` | Maximum waiting requests per server replica. `0` disables queueing. | | `QUEUE_WAIT_TIMEOUT` | `30s` | Maximum wait for a worker slot. | | `MAX_LIFETIME_SESSIONS` | `50` | Session claims before a worker drains and recycles. `0` disables recycling. | | `WORKER_HEARTBEAT_TTL` | `30s` | Time before an available worker with no heartbeat becomes stalled. | | `SESSION_HEARTBEAT_TTL` | `30s` | Time before a pending or running session with no heartbeat expires. | | `SESSION_HEARTBEAT_INTERVAL` | `10s` | How often a live relay renews its session. | | `PENDING_SESSION_TTL` | `30s` | Maximum time a claimed session can remain pending. | | `STALLED_WORKER_TTL` | `10m` | Time before an idle stalled worker record is removed. | | `RESCUER_INTERVAL` | `5s` | Base interval between recovery sweeps, with 20% jitter. | | `WORKER_DIAL_TIMEOUT` | `10s` | Total time limit for the server’s WebSocket dial to a worker. | | `RELAY_WRITE_TIMEOUT` | `30s` | Time limit for each relay write. | | `RELAY_PING_INTERVAL` | `20s` | Interval between relay WebSocket pings. | | `RELAY_PONG_TIMEOUT` | `60s` | Maximum time without data or a pong from a peer. | | `SHUTDOWN_GRACE_PERIOD` | `20s` | Time allowed for active relays to finish after shutdown begins. | `SESSION_HEARTBEAT_INTERVAL` must be less than `SESSION_HEARTBEAT_TTL`. `RELAY_PING_INTERVAL` must be less than `RELAY_PONG_TIMEOUT`. `WORKER_DIAL_TIMEOUT` must be less than the 15-second session reconciliation grace period. The server rejects an invalid timeout configuration. Budget `pool_max_conns + 1` PostgreSQL connections per server replica. The extra connection listens for capacity notifications. Each replica has its own admission queue. Notifications and a one-second polling fallback wake waiting requests when capacity changes. The `serve` command applies migrations before it starts the HTTP server. API-key commands connect to the database but do not apply migrations. ## Worker [Section titled “Worker”](#worker) Worker time values are integer **seconds**. | Variable | Default | Purpose | | -------------------- | ---------------- | -------------------------------------------------------------------------------------------------------- | | `SERVER_URL` | Required | Server HTTP or HTTPS URL. | | `WORKER_API_KEY` | Unset | Bearer key for server requests. Required after authentication is enabled. | | `BROWSER_TYPE` | `chromium` | One of `chromium`, `firefox`, or `webkit`. | | `PORT` | `3131` | Port for browser relay connections. | | `PRIVATE_HOSTNAME` | Machine hostname | Address advertised to the server. It must be reachable from the server. | | `MAX_SLOTS` | `5` | Concurrent sessions per worker, from 1 to 1024. | | `HEADLESS` | `true` | Run the browser without a visible window. | | `HEARTBEAT_INTERVAL` | `5` | Interval between worker heartbeats. | | `DRAIN_TIMEOUT` | `300` | Maximum wait for active sessions during recycling or shutdown. Remaining sessions close when it expires. | | `LOG_LEVEL` | `info` | One of `debug`, `info`, `warn`, or `error`. | | `LOG_FORMAT` | `json` | One of `json` or `text`. | The supplied Compose file sets `DRAIN_TIMEOUT=30` and a 60-second container stop grace period. If you increase the drain timeout, keep the stop grace period above `DRAIN_TIMEOUT + 20` seconds for cleanup. Browser command-line flags are fixed when a worker starts. Set session options such as proxy, locale, viewport, and cookies through Playwright browser contexts. See [scaling](/browserthing/docs/scaling/) for slot tuning and remote worker examples. # Connect with Playwright > Connect your application's Playwright code to BrowserThing from Node.js or Python. Use Playwright’s `connect()` method in your application with the BrowserThing WebSocket URL. The server selects an available worker that matches the browser type and the client’s Playwright major and minor version. ```text Application -> Playwright commands -> Browser worker <- Results <- ``` BrowserThing v0.6.0 images use Playwright **1.63.0**. The examples below use that version. Use `wss://` when your endpoint has TLS. The examples save a screenshot as a small browser task. Use the same connection for the browser actions your application needs. ## Node.js [Section titled “Node.js”](#nodejs) ```sh npm install playwright@1.63.0 ``` Save this as `connect.mjs`: ```js import { chromium } from 'playwright'; const browser = await chromium.connect('ws://localhost:8080'); try { const context = await browser.newContext({ viewport: { width: 1280, height: 720 }, }); const page = await context.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: 'preview.png', fullPage: true }); } finally { await browser.close(); } ``` Run `node connect.mjs`. It saves `preview.png` in the current directory on the client machine. Replace `https://example.com` with your application’s page URL. ## Python [Section titled “Python”](#python) ```sh python -m pip install playwright==1.63.0 ``` Save this as `connect.py`: ```python import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser = await p.chromium.connect("ws://localhost:8080") try: context = await browser.new_context() page = await context.new_page() await page.goto("https://example.com") await page.screenshot(path="preview.png", full_page=True) finally: await browser.close() asyncio.run(main()) ``` Run `python connect.py`. It saves `preview.png` in the current directory on the client machine. Java and .NET clients use their corresponding `BrowserType.connect()` / `ConnectAsync()` methods with the same endpoint and version requirement. ## PDF exports [Section titled “PDF exports”](#pdf-exports) Use a Chromium worker for PDF generation. In either example above, replace the screenshot call with a PDF call after the page has loaded: ```js await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true }); ``` ```python await page.pdf(path="report.pdf", format="A4", print_background=True) ``` Playwright saves `report.pdf` on the client machine. [PDF generation](https://playwright.dev/docs/api/class-page#page-pdf) uses print styles by default. To use screen styles, call `page.emulateMedia({ media: 'screen' })` in Node.js or `page.emulate_media(media="screen")` in Python before the PDF call. For an application response or file upload, omit `path` from `screenshot()` or `pdf()` and use the returned bytes. Your application decides where to store or send the result. ## Firefox and WebKit [Section titled “Firefox and WebKit”](#firefox-and-webkit) Start a worker with `BROWSER_TYPE=firefox` or `BROWSER_TYPE=webkit` first. A worker serves one browser type. Then select it in the connection URL: ```js import { firefox, webkit } from 'playwright'; const firefoxBrowser = await firefox.connect( 'ws://localhost:8080/?browser=firefox', ); await firefoxBrowser.close(); const webkitBrowser = await webkit.connect( 'ws://localhost:8080/?browser=webkit', ); await webkitBrowser.close(); ``` The repository’s [local Compose file](https://github.com/mbroton/browserthing/blob/v0.6.0/docker-compose.local.yaml) shows a stack with all three browser types. It builds from source and requires a repository checkout. ## API keys [Section titled “API keys”](#api-keys) After you create an API key, clients must send it with each connection. For a Node.js client, use an authorization header: ```js const browser = await chromium.connect('wss://grid.example.com', { headers: { Authorization: `Bearer ${process.env.BROWSERTHING_API_KEY}` }, }); ``` Clients can also pass `?token=pwd_...` in the URL. Keep keys out of source control and shared logs. Every valid key has full access to the service in v0.6.0. See [security](/browserthing/docs/security/). ## Session lifetime [Section titled “Session lifetime”](#session-lifetime) Create your own browser context after you connect. Close the browser connection in a `finally` block so the worker can release the session slot. A session ends when its connection closes. Reconnecting to a completed session is not supported in v0.6.0. The [API guide](/browserthing/docs/api/) explains how to create a pending session and attach to it by ID. # Deployment > Deploy an internal browser service for your applications with authentication, private networking, and persistent data. Start with the [quick start](/browserthing/docs/quick-start/). For a deployment used by other machines, configure the database password, API keys, and network access before you expose the server. You operate and update this service for your applications. To move browser CPU and memory use off application servers, deploy workers on separate hosts. The [scaling guide](/browserthing/docs/scaling/) shows the required network connections. ```text Apps -> TLS proxy -> Server -> private browser workers | v PostgreSQL Workers -> Server: registration and heartbeats ``` ## Database password and storage [Section titled “Database password and storage”](#database-password-and-storage) Before the **first** start, put a strong `POSTGRES_PASSWORD` in a `.env` file next to `docker-compose.yaml`. The Compose file passes it to PostgreSQL and the server’s database connection string. PostgreSQL reads this password when it initializes an empty volume. Changing `.env` later does not change the password in an existing database. Use normal PostgreSQL password rotation procedures for an existing installation. Keep the `postgres-data` volume and back it up. It contains sessions, workers, and API keys. Use `docker compose down` to stop the deployment while retaining the volume. The `-v` option deletes volumes and their data. ## Create an API key [Section titled “Create an API key”](#create-an-api-key) With the server still bound to localhost, create the first key: ```sh docker compose exec server server apikey create --name grid ``` Save the returned key. Creating it immediately enables authentication across the service. Set the worker key in `.env`: ```dotenv WORKER_API_KEY=pwd_replace_with_your_key ``` Recreate the workers so they receive the key: ```sh docker compose up -d --force-recreate worker ``` Give clients a key before they connect. Workers also need keys for registration and heartbeats. Do not commit `.env` or keys to Git. Equal access Every valid API key has full browser and control-plane access in v0.6.0. Keys do not separate tenants or restrict access to individual sessions. ## Network access [Section titled “Network access”](#network-access) The server needs access to PostgreSQL and to every worker’s advertised WebSocket address. Workers need HTTP access to the server. Expose only the server to clients. Keep PostgreSQL and worker ports on private networks. Put the client endpoint behind a TLS reverse proxy or a suitable private network. API keys do not encrypt `http://` or `ws://` traffic. A reverse proxy must support WebSocket upgrades and timeouts that allow your sessions to run. If the proxy runs on the same host, it can reach the default `127.0.0.1:8080` mapping. A proxy in a separate container needs a shared Docker network or another route to the server. ## Health and shutdown [Section titled “Health and shutdown”](#health-and-shutdown) * `GET /healthz` checks process health. * `GET /readyz` checks database readiness. * `GET /v1/capacity` reports browser slots and queue depth; it needs a key after authentication is enabled. Keep the Compose stop grace period longer than the application’s shutdown window. The supplied file uses 60 seconds and sets worker `DRAIN_TIMEOUT=30`. The worker’s standalone default drain timeout is 300 seconds. For workers on separate hosts, follow [scaling](/browserthing/docs/scaling/). # Quick start > Start BrowserThing with Docker Compose and connect your application code to a browser worker with Playwright. You need Docker with the Compose plugin, `curl`, and Node.js 20 or later for the client example. BrowserThing runs its browsers in containers. You do not need to install browsers on the client machine. This local setup runs the service and its browser worker on your machine. For your application deployment, you can move workers to [separate hosts](/browserthing/docs/scaling/#add-workers-on-other-hosts) so browser CPU and memory use stays off your application servers. ## 1. Download the configuration [Section titled “1. Download the configuration”](#1-download-the-configuration) Create an empty directory for this deployment. Keep its name unchanged when you upgrade, because Docker Compose uses it to name the database volume. ```sh mkdir browserthing cd browserthing curl -fsSLO https://mbroton.github.io/browserthing/downloads/docker-compose.yaml curl -fsSL --create-dirs -o worker/seccomp_profile.json https://raw.githubusercontent.com/mbroton/browserthing/v0.6.0/worker/seccomp_profile.json ``` This site’s Compose file pins both BrowserThing images to `0.6.0`. It starts PostgreSQL, the server, and one Chromium worker with five session slots. ## 2. Start the service [Section titled “2. Start the service”](#2-start-the-service) ```sh docker compose up -d docker compose ps curl -fsS http://localhost:8080/v1/capacity ``` The first start downloads the images. If the capacity request fails or shows no workers, wait for startup and try again. Check `docker compose logs` if the worker does not register. Local setup The server listens on `127.0.0.1:8080`. This example uses the Compose file’s local database password and starts without API keys. Follow the [deployment guide](/browserthing/docs/deployment/) before you expose the service to other machines. ## 3. Run a browser task [Section titled “3. Run a browser task”](#3-run-a-browser-task) Install the matching Playwright client: ```sh npm init -y npm install playwright@1.63.0 ``` Save this as `preview.mjs`. This example saves a screenshot. Replace the task with the browser actions your application needs: ```js import { chromium } from 'playwright'; const browser = await chromium.connect('ws://localhost:8080'); try { const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: 'preview.png', fullPage: true }); } finally { await browser.close(); } ``` Run it: ```sh node preview.mjs ``` Open `preview.png` in the current directory to see the result. The browser runs on the worker, and Playwright saves the screenshot on the client machine. Closing the connection releases the session’s resources for the next task. The client and worker must have the same Playwright **major and minor** version. For these images, use `1.63.x`. ## 4. Add capacity or stop [Section titled “4. Add capacity or stop”](#4-add-capacity-or-stop) Start three workers for 15 concurrent session slots: ```sh docker compose up -d --scale worker=3 ``` Stop the service and keep its database volume: ```sh docker compose down ``` Continue with [connection examples](/browserthing/docs/connect/) or [deployment](/browserthing/docs/deployment/). # Scaling > Add browser workers and tune session capacity on one host or several hosts. Scale browser capacity separately from your application instances. Put workers on separate hosts to move browser CPU and memory use off application servers. Capacity comes from the number of workers and the slot limit on each worker. For example, three workers with `MAX_SLOTS=5` provide 15 concurrent session slots. Actual throughput depends on CPU, memory, and the pages you open. ## Add workers on one host [Section titled “Add workers on one host”](#add-workers-on-one-host) ```sh docker compose up -d --scale worker=3 ``` The Compose file does not publish worker ports or assign fixed worker names. This lets Compose create several workers. Each worker registers itself. ## Add workers on other hosts [Section titled “Add workers on other hosts”](#add-workers-on-other-hosts) ```text Apps -> Server + PostgreSQL (host A) | +-> Workers (host B) +-> Workers (host C) ``` Configure [authentication and private networking](/browserthing/docs/deployment/) first. The server must be able to reach each worker at its advertised hostname and port. On a worker host, download the [Chromium seccomp profile](https://raw.githubusercontent.com/mbroton/browserthing/v0.6.0/worker/seccomp_profile.json) to `worker/seccomp_profile.json`. Put `WORKER_API_KEY` in `.env`. Then use a worker-only Compose file like this: ```yaml services: worker: image: ghcr.io/mbroton/browserthing/worker:0.6.0 init: true security_opt: - seccomp=./worker/seccomp_profile.json shm_size: "1gb" stop_grace_period: 60s ports: - "3131:3131" environment: - SERVER_URL=http://host-a.internal:8080 - WORKER_API_KEY=${WORKER_API_KEY} - PRIVATE_HOSTNAME=host-b.internal - PORT=3131 - BROWSER_TYPE=chromium - MAX_SLOTS=5 - DRAIN_TIMEOUT=30 restart: unless-stopped ``` Replace both example hostnames with addresses on your private network. Restrict port 3131 to the server. Registration alone does not prove that the server can connect back to the worker. For several workers on this host, use separate services with different `PORT` values and matching port mappings. The fixed port mapping above cannot be shared by scaled replicas. ## Tune slots [Section titled “Tune slots”](#tune-slots) Start with the default of five slots. For tasks that keep the CPU busy, the project’s starting guideline is about two slots per available CPU. Tasks that spend more time waiting for pages can use more slots, provided memory permits. Increase capacity in steps. Measure latency, throughput, and memory with your own workload. Adding more workers on a fully used host does not add CPU capacity. The repository includes [benchmark scripts](https://github.com/mbroton/browserthing/tree/v0.6.0/scripts/bench). See [benchmark results](/browserthing/docs/benchmarks/) for a comparison of BrowserThing and Browserless on the same hardware. ## Watch the queue [Section titled “Watch the queue”](#watch-the-queue) `GET /v1/capacity` reports free slots by browser type and the server replica’s queue depth. A queue that stays above zero, or repeated 429 responses, indicates that you should inspect capacity and worker health. The default queue limit is 100 requests, with a 30-second wait timeout. These limits apply per server replica. See the [configuration reference](/browserthing/docs/configuration/). # Security > Understand BrowserThing authentication, browser isolation, and network boundaries. BrowserThing is built for applications you trust. It trusts authenticated clients. In bootstrap mode, it trusts every client that can reach the server. Browsers can visit untrusted pages, but the service is not a security boundary between hostile clients. ## Session isolation [Section titled “Session isolation”](#session-isolation) ```text Worker container └── Shared browser process ├── Session A -> separate browser contexts └── Session B -> separate browser contexts ``` Contexts separate cookies and storage. They do not provide separate operating system processes for each session. If the shared browser crashes, the worker’s sessions end together. Use dedicated VMs when you need a stronger boundary against hostile tenants or browser exploits. An authenticated client can use browser network access, so control which internal addresses workers can reach. Browser tasks can load a URL supplied by a user. Apply network access controls to the workers for these requests, including access to internal services. ## Authentication [Section titled “Authentication”](#authentication) A server with zero active API keys starts in open bootstrap mode. Create the first key to enable authentication. Every valid key has equal access, including the ability to inspect or delete other sessions. Once locked, the running server stays locked even if you revoke all keys. If you restart it with zero active keys in the database, it returns to bootstrap mode. Plan key rotation before you revoke your last usable key. ## Transport and network access [Section titled “Transport and network access”](#transport-and-network-access) Use TLS (`https://` and `wss://`), a VPN, or an appropriate private network. Authentication does not encrypt traffic. Keep the database and workers private. The supplied Compose file binds the server to localhost for initial setup. The server accepts API keys in the authorization header or a WebSocket URL’s `token` parameter. Its request logs omit query strings, but other tools and proxies may record URLs. Use headers where your client supports them. ## Container configuration [Section titled “Container configuration”](#container-configuration) The worker image runs as the non-root `pwuser` user. The supplied Compose file uses Playwright’s Chromium seccomp profile. Keep these settings when you adapt the deployment. See [Playwright’s Docker guidance](https://playwright.dev/docs/docker). Read the [deployment guide](/browserthing/docs/deployment/) to configure keys and network access. # Troubleshooting > Diagnose failed connections, missing workers, and capacity errors. Start with the service state and logs: ```sh docker compose ps docker compose logs server worker curl -fsS http://localhost:8080/readyz ``` ## The client cannot connect [Section titled “The client cannot connect”](#the-client-cannot-connect) ```text Server ready? -> API key accepted? -> Worker registered? -> Browser and Playwright versions match? -> Server can reach worker? ``` Check the endpoint and browser type. `chromium.connect()` needs a Chromium worker. Firefox and WebKit need their own workers and `?browser=` selection. Check the client’s Playwright version against the worker. The major and minor versions must match. BrowserThing v0.6.0 images use Playwright 1.63.0. ## HTTP errors [Section titled “HTTP errors”](#http-errors) REST requests and failed WebSocket connections can return these statuses: | Status | Cause | Next action | | ------ | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `401` | The API key is missing, invalid, or revoked. | Give clients and workers a valid key. | | `409` | A session already has a client, or the attaching client’s Playwright version does not match. | Read the error message. Use a new session or a matching client version. | | `410` | The session is completed, failed, or expired. | Create a new session. A closed session cannot be resumed. | | `422` | A REST request has invalid fields, or requests `mode: dedicated`, which is not supported. | Check the response details. Omit `mode` or use `default`; check the browser type and version. | | `429` | All matching capacity is busy and the queue is full or disabled. | Close unused sessions, reduce concurrency, or add workers. | | `503` | A queued request timed out, the server is shutting down, or the authentication service is unavailable. | Read the error body. Check worker availability, browser/version matches, database access, and server logs. Admission errors include `Retry-After: 1`. | The `409` and `410` statuses apply when attaching to `/sessions/{id}`. Read a REST error body with `curl -sS -i`; `curl -f` hides the response body. ## Requests return 401 [Section titled “Requests return 401”](#requests-return-401) The server requires a key after you create the first API key. Give workers `WORKER_API_KEY`, and give clients an authorization header or a `token` query parameter. Recreate workers after you change their environment variables. ## A remote worker registers but connections fail [Section titled “A remote worker registers but connections fail”](#a-remote-worker-registers-but-connections-fail) The worker can reach the server, but the server may not be able to reach the worker. Set `PRIVATE_HOSTNAME` to an address the server can resolve and reach. Publish the configured `PORT` and permit traffic from the server through the worker host’s firewall. ## Requests wait or return 429 [Section titled “Requests wait or return 429”](#requests-wait-or-return-429) Inspect `GET /v1/capacity` with an API key if needed. Check available slots, queue depth, and worker health. Close client connections after use. Add workers if the workload needs more capacity, and measure CPU and memory before you increase `MAX_SLOTS`. For a `503` queue timeout, also check that a worker matches the requested browser and Playwright version. Free slots on a different browser or version cannot serve the request. Admission waits up to `QUEUE_WAIT_TIMEOUT` (30 seconds by default). After `Retry-After`, use a limited retry count with increasing delays. ## Several sessions end together [Section titled “Several sessions end together”](#several-sessions-end-together) Sessions on one worker share a browser process. A browser crash ends those sessions. Recycling also closes remaining sessions when `DRAIN_TIMEOUT` expires. A server restart ends connections on that relay. Check logs before you retry work, especially if the automation changes data on other sites. A new connection starts a new session; it does not resume the earlier task. ## Chromium fails during startup [Section titled “Chromium fails during startup”](#chromium-fails-during-startup) Use the supplied Compose file with `init: true`, the seccomp profile, and shared memory settings. Keep the worker’s non-root user. Check the worker logs for the startup failure instead of disabling the sandbox. If you file a [GitHub issue](https://github.com/mbroton/browserthing/issues), include the BrowserThing and Playwright versions, browser type, and relevant logs. Remove API keys and private URLs before you share them. # Upgrading > Update BrowserThing images and preserve your deployment configuration and data. Update the server and workers together. Back up the database and plan for active sessions to end when the containers restart. ## Choose a release [Section titled “Choose a release”](#choose-a-release) | Component | Image for v0.6.0 | | --------- | ------------------------------------------- | | Server | `ghcr.io/mbroton/browserthing/server:0.6.0` | | Worker | `ghcr.io/mbroton/browserthing/worker:0.6.0` | Read the [release notes](https://github.com/mbroton/browserthing/releases) before you change the image tags. Pin both images to the same BrowserThing release. ## Preserve the Compose project and database [Section titled “Preserve the Compose project and database”](#preserve-the-compose-project-and-database) Keep your existing `.env` file and Compose project name. If you change the deployment directory name, find the current project name first: ```sh docker compose ls ``` Set that same name in `.env` before you start the deployment in the new directory: ```dotenv COMPOSE_PROJECT_NAME=your_existing_project_name ``` This keeps Compose connected to the existing PostgreSQL volume. Back up the database before an upgrade. Do not use `docker compose down -v` to upgrade; it removes the volumes. ## Pull and restart [Section titled “Pull and restart”](#pull-and-restart) Update both image references in your existing Compose file. Plan for active sessions to end when the server or workers restart. ```sh docker compose pull server worker docker compose up -d docker compose ps ``` The server applies database migrations when it starts. Check server and worker logs, then run a client connection using a compatible Playwright version. ## Check client compatibility [Section titled “Check client compatibility”](#check-client-compatibility) The client and worker need matching Playwright major and minor versions. The v0.6.0 worker uses **1.63.0**. Update client dependencies when a new worker image changes that version.