Supported Providers
browser-gateway works with any WebSocket endpoint. If your browser provider exposes a WebSocket URL, it works with the gateway.
browser-gateway works with any WebSocket endpoint. If your browser provider exposes a WebSocket URL, it works with the gateway.
How Providers Work
Each provider is a WebSocket URL. The gateway forwards bytes transparently and doesn't parse what's behind it. You configure the URL (with any auth params baked in), set connection limits, and assign a priority.
providers:
my-provider:
url: <websocket-url-with-auth>
limits:
maxConcurrent: <number>
priority: <number>Provider Types
Remote browser services (cloud)
Cloud providers typically give you a WebSocket URL with an API key:
providers:
cloud-provider:
url: wss://provider.example.com?token=${API_TOKEN}
limits:
maxConcurrent: 5
priority: 1browserserve (self-hosted runtime)
browserserve is a self-hosted browser server from the same stack: one container hands out isolated Chrome sessions with a warm pool, host-derived capacity, and an optional profile channel. It is the only self-host option that carries a full profile, including IndexedDB and service workers, across sessions.
docker run -d -p 9222:9222 --shm-size=1g ghcr.io/browser-gateway/browserserve:latestConfig:
providers:
browserserve:
url: ws://your-host:9222
limits:
maxConcurrent: 8
priority: 1The gateway auto-detects a browserserve provider and adopts its advertised capacity when maxConcurrent is left unset. See the browserserve reference for configuration, the profile channel, and isolation tiers.
Self-hosted Playwright
Run your own Playwright server:
npx playwright run-server --port 4000 --host 0.0.0.0Or via Docker:
docker run -d -p 4000:3000 --shm-size=1gb \
mcr.microsoft.com/playwright:v1.50.1-noble \
/bin/sh -c "npx -y playwright@1.50.1 run-server --port 3000 --host 0.0.0.0"Config:
providers:
my-playwright:
url: ws://playwright-host:4000
limits:
maxConcurrent: 10
priority: 1Important: Playwright client and server versions must match exactly.
Raw Chrome (--remote-debugging-port)
Chrome/Chromium with remote debugging enabled:
google-chrome --remote-debugging-port=9222 --headless --no-sandboxGet the WebSocket URL:
curl http://localhost:9222/json/version
# Look for "webSocketDebuggerUrl"Config:
providers:
my-chrome:
url: http://chrome-host:9222 # Auto-discovers the WebSocket endpoint
limits:
maxConcurrent: 1
priority: 1Use http:// for Chrome CDP, the gateway auto-discovers the full WebSocket path via /json/version. You can also use the full ws:// URL if you already have it.
Managed browsers (cloud)
On the cloud you can also use browsers we run, with no provider account: as a provider or as a fallback when your own providers fail or are full. Paid per second from your credits. See Managed browsers.
Connection Modes
The gateway is protocol-agnostic - it forwards raw bytes without parsing. However, your client needs to use the correct connection method for the provider type:
| Provider Type | Client Method |
|---|---|
| Playwright run-server | chromium.connect(wsEndpoint) |
| Chrome CDP endpoints | chromium.connectOverCDP(wsEndpoint) |
| Puppeteer (any) | puppeteer.connect({ browserWSEndpoint }) |
Multiple Providers
Mix any number of providers with different priorities:
providers:
primary:
url: wss://provider-a.example.com?key=${KEY_A}
limits:
maxConcurrent: 5
priority: 1
overflow:
url: ws://my-playwright-server:4000
limits:
maxConcurrent: 20
priority: 2
emergency:
url: wss://provider-b.example.com?key=${KEY_B}
limits:
maxConcurrent: 50
priority: 3The gateway tries them in priority order. If the primary is full or down, traffic routes to the next available provider automatically.
Turning A Provider Off
Take a provider out of rotation without deleting it. Its URL, key and settings stay in gateway.yml, so switching it back on restores it exactly as it was.
When to use this: a provider is having an outage, you are over its monthly quota, or you want to test the rest of your setup without it.
providers:
primary:
url: wss://provider-a.example.com?key=${KEY_A}
enabled: falseWhile a provider is off, the gateway behaves as if it was never added:
- New sessions never go to it, whatever the routing strategy.
- A request that names it with
?provider=primarygets the same400as a provider that does not exist. - It is not health-checked and does not appear in
/v1/status, the playground or the API page. - Sessions that were already running on it finish normally.
It still appears in GET /v1/providers with enabled: false, and on the dashboard Providers page with a switch to turn it back on. From the API:
curl -X PUT http://localhost:9500/v1/providers/primary \
-H "Authorization: Bearer $BG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'Omitting enabled means the provider is on.
Provider Capacity
Every provider has a real concurrency ceiling. Set maxConcurrent to the smaller of what the provider actually handles and what you have paid for.
Browserless (self-hosted)
The ghcr.io/browserless/chromium container on an 8 GB / 8 vCPU host holds 5 concurrent sessions safely, with 6 as the absolute ceiling. Above 6, incoming sessions serialize inside Browserless's internal multiplexer and clients start hitting timeouts.
We verified this by ramping load until sessions failed. At the failure point, host RAM peaked at 21% and CPU at 6%, so the cliff is not resource-bound. Browserless documentation claims 10 sessions per GB of RAM; the observed number is 0.625 per GB.
providers:
browserless-1:
url: wss://browserless-1.example.com?token=${TOKEN_1}
limits:
maxConcurrent: 5
browserless-2:
url: wss://browserless-2.example.com?token=${TOKEN_2}
limits:
maxConcurrent: 5To serve N concurrent sessions on Browserless, provision ceil(N / 5) instances. If your traffic bursts (many session opens within a few seconds), keep at least 20% headroom below the fleet ceiling. Simultaneous saturation across every slot in the fleet can push individual instances briefly above their per-instance ceiling and trigger the serialization behavior.
Bump TIMEOUT on Browserless to a real value (default is 30000 ms, which kills sessions at 30 seconds regardless of activity):
docker run -e TIMEOUT=3600000 ghcr.io/browserless/chromiumbrowserserve
browserserve's /json/version endpoint advertises the concurrency the host can actually sustain, measured on the host at startup. The gateway reads this on provider add and adopts it when maxConcurrent is left unset.
Other providers
Ask the vendor for the real number, not the marketing number. If in doubt, ramp your own load until sessions start failing and set maxConcurrent to 80% of the last clean level.
Custom Headers
Some providers require an Authorization: Bearer <key> header on the WebSocket upgrade instead of a URL query token. Configure custom headers per provider:
providers:
cloud-bearer:
url: wss://provider.example.com/cdp
headers:
Authorization: Bearer ${API_KEY}
X-Client: browser-gatewayHeaders are sent on the outbound WebSocket handshake to the upstream. Values interpolate from environment variables. Limits: 20 headers max, key up to 80 chars, value up to 4 KB. See Configuration → Custom headers for merge order.
How Provider Credentials Are Shown Back To You
Provider URLs and headers usually carry a key of some kind. When you list providers
over the API or view them in the dashboard, those values come back masked as ***,
so nothing that can open a browser session is echoed out of the gateway. What gets
masked:
- Query parameters in a provider URL whose name looks like a credential:
token,apiKey,api_key,key,secret,password,access_token,jwt,sigand similar. - A username or password written into the URL itself, as in
wss://user:pass@host. - The value of any header whose name carries a credential, such as
Authorization,X-Api-KeyorCookie.
{
"providers": [
{
"id": "cloud-primary",
"url": "wss://provider.example.com/?token=***",
"headers": { "Authorization": "***", "X-Client": "browser-gateway" },
"maxConcurrent": 5,
"priority": 1
}
]
}The real values stay in gateway.yml and are still sent to the provider. If you send
a masked value straight back in a PUT /v1/providers/:id, the gateway keeps what it
already had rather than storing ***, so editing one field of a provider in the
dashboard does not wipe the others. To change a credential, send the new value.
Profiles And Providers
Any provider can load and save any profile. Setting profile: on a provider only decides which sessions it takes. A browser that still holds a different profile is refused, and on a provider that reuses browsers, anything an earlier session left behind can be saved with your profile.
See Profiles → Providers And Profiles for what gets saved and the residue check.
Tips
- Auth goes in the URL or in headers. Query params (
?token=xxx) work for most providers. Useheaders:forAuthorization: Bearerschemes. Always reference secrets via${ENV_VAR}. - Set realistic limits. Check the provider's actual concurrency limit and set
maxConcurrentaccordingly. The gateway enforces these locally to avoid hitting provider rate limits. - Chrome CDP needs the full path. Chrome's CDP URL includes a UUID that changes on restart. Point at
http://host:9222and the gateway auto-discovers the current WebSocket path via/json/version.
Configuration Reference
browser-gateway is configured via a gateway.yml file, environment variables, or both.
browserserve
browserserve is a self-hosted browser server. One container runs isolated Chrome sessions over CDP, with an optional profile channel to save and restore session state. Run it standalone, or add it to the gateway as a provider.