April 15, 2026 · 4 min read
What is a browser router?
One connection URL. Every browser provider. The routing layer that sits between your app code and every headless browser you already pay for.
If you've ever wired a Stagehand, browser-use, Puppeteer, or Playwright client to more than one browser provider, you've written a browser router. Probably by accident. It looked like a try/catch that fell back to a different browserWSEndpoint, or a config file with PRIMARY_URL and BACKUP_URL, or a helper function that picked between Browserless and Steel based on which was less saturated last time you checked.
That helper function is the router. It's the layer between your app code and the actual browsers.
A browser router is a service that sits in front of every browser provider you use (Browserless, Steel, Browserbase, Cloudflare Browser Rendering, or self-hosted Chrome), exposes one connection URL, and picks which upstream to route each session to based on rules you define. Your app connects once. The router decides where the traffic actually goes.
Why the routing layer exists at all
Every headless-browser project eventually hits the same three problems:
-
One provider is never enough. Browserless is fast on connect but has strict rate limits. Steel handles anti-bot workloads. Cloudflare Browser Rendering has the lowest per-session price floor. Self-hosted Chrome is free but hard to scale. The moment you use two providers, your app code has to know which is which. Swapping providers means rewriting your app.
-
Providers go down. Everyone runs on infrastructure. Everyone has bad days. If your fleet is pinned to one provider, their outage is your outage. If it isn't, someone needs to know when to fail over and where to fail over to.
-
Cost isn't uniform. A 30-second screenshot session and a 30-minute scraping run have wildly different economics on different providers. Sending them to the same one is throwing money away.
A router centralizes those three concerns into one layer. Your app code doesn't change. The routing decisions live somewhere your ops team can reason about.
What a browser router actually does
Concretely, when your client connects, the router:
- Filters providers that are healthy and not in cooldown
- Applies capacity gates so you don't overwhelm a provider that's already at its concurrent limit
- Picks one based on the strategy you configured (priority chain, round-robin, least-connections, weighted, latency-optimized)
- Forwards the WebSocket connection to the chosen upstream
- Watches for failures and fails over to the next candidate if the connection drops
That's the sync path. The async path handles the interesting parts: persistent profiles (cookies + localStorage captured on session end, injected on next connect), session recording (CDP screencast frames captured, encoded to MP4 in the background), observability (per-session traces), and MCP integration (so AI agents can request browsers through the same layer).
Router vs provider vs pool
Three concepts get conflated. They're different:
- A browser provider is a service that runs Chrome for you. Browserless, Steel, Browserbase are providers. So is self-hosted Chrome behind a WebSocket.
- A browser pool is a set of pre-warmed browser instances on one machine, hand-rolled to reduce cold-start latency.
puppeteer-clusteris a pool. - A browser router is the layer above all of that. It routes to N providers, each of which might itself be pooled. Pools are per-machine. Routers are per-fleet.
You can run a router in front of one provider (still useful for observability + profile persistence). You can run a router in front of ten (routing + failover become the point). You can run a router in front of your own pool + external providers (start on self-hosted, burst to Browserless when capacity runs out).
Why now
Two things happened in 2025-2026 that made routers necessary rather than convenient:
-
AI agents ship real workloads. Every serious agent framework needs a browser. Cursor, Claude, Stagehand, browser-use, and any MCP client all connect to a CDP endpoint. The number of teams running headless browsers in production went up an order of magnitude in eighteen months, and each agent framework wants to route across whichever provider fits the task.
-
Provider count exploded. Browserless, Steel, Browserbase, Hyperbrowser, BrowserCat, Cloudflare Browser Rendering, Kitesurf, plus a growing OSS runtime layer. Each fits a different job. Nobody wants to pick one and marry it.
When multi-provider is the norm, the routing layer is the primitive that belongs one level down from the app code.
The MIT version
BrowserGateway is the open-source router for this shape. npm install -g browser-gateway or docker run ghcr.io/browser-gateway/server:latest. Stagehand, browser-use, Puppeteer, Playwright, and any MCP client work unchanged. Same connection URL, we handle the rest.
Configure providers in gateway.yml:
backends:
browserless-1:
url: wss://production-sfo.browserless.io/?token=${BROWSERLESS_TOKEN}
limits:
maxConcurrent: 5
priority: 1
steel-1:
url: wss://connect.steel.dev?apiKey=${STEEL_KEY}
limits:
maxConcurrent: 2
priority: 2
local-chrome:
url: ws://localhost:9222
priority: 3Your app connects to one URL and doesn't know or care which upstream is serving:
import puppeteer from "puppeteer-core";
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://localhost:9500/v1/connect",
});Failover, profile persistence, session recording, and the MCP server all run in the same process. MIT license. Repo: github.com/browser-gateway/browser-gateway.