browser-gateway

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: 1

browserserve (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:latest

Config:

providers:
  browserserve:
    url: ws://your-host:9222
    limits:
      maxConcurrent: 8
    priority: 1

The 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.0

Or 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: 1

Important: 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-sandbox

Get 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: 1

Use 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.

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 TypeClient Method
Playwright run-serverchromium.connect(wsEndpoint)
Chrome CDP endpointschromium.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: 3

The gateway tries them in priority order. If the primary is full or down, traffic routes to the next available provider automatically.

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 or Railway)

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: 5

To 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/chromium

browserserve

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

Headers 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.

Profiles And Providers

Whether a provider can safely carry a profile depends on how the underlying browser is isolated between sessions. The self-hosted runtime gives every session a fresh isolated Chrome and can serve any profile with no pinning. External CDP providers must pin one profile per slot with profile:. multiProfile: true on external providers is rejected at add-time.

See Profiles → Provider Isolation for the full matrix and the automatic residue detection that catches misconfigured or misbehaving providers.

Tips

  • Auth goes in the URL or in headers. Query params (?token=xxx) work for most providers. Use headers: for Authorization: Bearer schemes. Always reference secrets via ${ENV_VAR}.
  • Set realistic limits. Check the provider's actual concurrency limit and set maxConcurrent accordingly. 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:9222 and the gateway auto-discovers the current WebSocket path via /json/version.

On this page