15% off your first purchase with code WELCOME15, up to 25 GB.

See pricing

Integration · AI & workflow tools

How to use a proxy with Stagehand

Stagehand launches a local browser with localBrowser.launch({ proxy: { server } }), but in version 4 it refuses a proxy username or password with "Authenticated local browser proxies are not supported yet". The working route is a small local forwarder: the proxy-chain package opens an unauthenticated port on 127.0.0.1 that passes traffic on to the ARC gateway with your credentials, and Stagehand's browser points at that port.

Reviewed 30 Oct 2026

your proxy, as a URL
http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000

The short answer

  • Stagehand 4: proxy.username and proxy.password on a local browser throw an error. Stagehand's own docs list authenticated local proxies as unsupported.
  • Stagehand 3.7: the same fields were accepted, but in our test Chrome answered the 407 without credentials and the page never loaded.
  • anonymizeProxy() from proxy-chain turns http://user:pass@gateway into a local http://127.0.0.1:port with no credentials; pass that as server.
  • One forwarder and one browser per agent task, each with its own _session- handle, keeps every task on its own exit IP.

Setup

Set up Stagehand with a proxy, step by step

  1. 01

    Install Stagehand and proxy-chain

    Stagehand drives a locally installed Chrome. proxy-chain is a Node library that runs the forwarder inside your own process, so no extra service is needed.

    shell
    npm i @browserbasehq/stagehand proxy-chain zod
  2. 02

    Start a forwarder, launch the browser, check the exit IP

    Percent-encode the username and password when you build the upstream URL. The forwarder listens on a random local port and adds the credentials on every request it passes on. Stagehand.create() attaches the AI layer to the browser; pages come from browser.context.

    check-exit.ts
    import { localBrowser, Stagehand } from "@browserbasehq/stagehand";import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain";const upstream =  "http://" + encodeURIComponent("USERNAME_pool-arcfast_country-de_session-k3f9a1x2_sesstime-10") +  ":" + encodeURIComponent("PASSWORD") + "@proxy.arcproxies.io:8000";const local = await anonymizeProxy(upstream);   // http://127.0.0.1:<port>const browser = await localBrowser.launch({ headless: true, proxy: { server: local } });const stagehand = await Stagehand.create({  browser,  model: { modelName: "openai/gpt-5.4-mini", apiKey: process.env.OPENAI_API_KEY },});const [page] = await browser.context.pages();const response = await page.goto("https://api.ipify.org?format=json");console.log(response ? await page.evaluate(() => document.body.innerText) : "no response");await browser.close();await closeAnonymizedProxy(local, true);
  3. 03

    One session per agent task

    An agent that searches, filters and opens results should keep one IP from start to finish. Wrap the forwarder and the browser in a helper that mints a handle per task and cleans both up afterwards, then run act(), observe() and extract() inside it.

    task.ts
    import { randomBytes } from "node:crypto";import { localBrowser, Stagehand } from "@browserbasehq/stagehand";import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain";const BASE = "USERNAME_pool-arcfast_country-de";   // pool + country; session added per taskexport async function withSession<T>(task: (s: Stagehand) => Promise<T>): Promise<T> {  const user = BASE + "_session-" + randomBytes(4).toString("hex") + "_sesstime-15";  const local = await anonymizeProxy(    "http://" + encodeURIComponent(user) + ":" + encodeURIComponent("PASSWORD") + "@proxy.arcproxies.io:8000",  );  const browser = await localBrowser.launch({ headless: true, proxy: { server: local } });  try {    const stagehand = await Stagehand.create({      browser,      model: { modelName: "openai/gpt-5.4-mini", apiKey: process.env.OPENAI_API_KEY },    });    return await task(stagehand);  } finally {    await browser.close();    await closeAnonymizedProxy(local, true);  }}
  4. 04

    Treat a missing response as a proxy failure

    When the gateway rejects the credentials, page.goto() does not throw: Chrome shows its own error page (ERR_TUNNEL_CONNECTION_FAILED) and the call resolves without a response. Check for that before letting the model act on the page, or it will try to work with an error screen.

    guard.ts
    const response = await page.goto(url);if (!response || response.status() >= 400) {  throw new Error("load failed: " + (response ? response.status() : "proxy or network error"));}

Why Stagehand needs a forwarder for proxy authentication

Chrome's own proxy setting has no place for credentials; a proxy that asks for them sends a 407 challenge, and something has to answer it. Playwright and Puppeteer answer it over the DevTools protocol, which is why their proxy options take a username. Stagehand 4 does not handle that challenge for local browsers and rejects the fields up front.

The forwarder moves the answer out of the browser. Chrome talks to 127.0.0.1 without credentials; the forwarder adds the Proxy-Authorization header on each request to the gateway. HTTPS stays end to end: the forwarder relays the CONNECT tunnel and never sees page content.

Stagehand versions and proxy options compared

SetupProxy with username and passwordResult in our test
Stagehand 4, localBrowser.launch({ proxy: { username, password } })RejectedThrows before the browser starts
Stagehand 3.7, localBrowserLaunchOptions.proxy with credentialsAccepted by the typeChrome received 407 and sent no credentials
Stagehand 4 + proxy-chain forwarderCredentials held by the forwarderPage loaded through the gateway

If you run an older Stagehand with the documented username and password fields, test with an IP echo page before trusting it.

Sessions, timing and bandwidth for browser agents

Agents are slow: every act() call waits for a model response, so a five-step task can take minutes. Set _sesstime- longer than the task's worst case, and keep in mind that a sticky exit is held on a best-effort basis; a changed IP mid-task should trigger a retry of the task, not a crash. AI agents covers patterns for this, and the browser-use guide shows the same idea in Python.

A local branded Chrome also makes its own background requests, for updates and Google services, and through a forwarder those count against your balance like page traffic. In our test they started within seconds of launch.

The problem

Common pitfalls

Credentials in the server URL

server: "http://user:pass@host:port" does not help: Chrome ignores credentials embedded in proxy settings. Keep them in the forwarder's upstream URL.

Leaking forwarders

Each anonymizeProxy() call opens a local port. Close it with closeAnonymizedProxy(url, true) in a finally block, or long-running workers accumulate open ports.

Sharing one forwarder across parallel tasks

A forwarder is bound to one upstream username, so all browsers behind it share one session handle and one exit IP. Start one forwarder per task.

Unencoded credentials

A password containing @ or : breaks the upstream URL. Wrap both parts in encodeURIComponent() as in the examples.

FAQ

Questions

Something not covered? Write to contact@arcproxies.io.

Does Stagehand support authenticated proxies?

Not for local browsers in Stagehand 4: setting proxy.username or proxy.password throws "Authenticated local browser proxies are not supported yet". A local forwarder such as proxy-chain's anonymizeProxy() holds the credentials instead, and Stagehand uses the forwarder's credential-free address.

Where do I set a proxy in Stagehand?

In Stagehand 4, pass proxy: { server, bypass } to localBrowser.launch(). In Stagehand 3, the same object went under localBrowserLaunchOptions in the Stagehand constructor.

How do I give each Stagehand agent its own IP?

Start one forwarder per task with a different _session- handle in the ARC username, and launch one browser behind each. The exit IP then stays the same for that task and differs from the others.

Why does my Stagehand page show ERR_TUNNEL_CONNECTION_FAILED?

The proxy refused the tunnel, most often because of a wrong username or password in the forwarder's upstream URL. page.goto() resolves without a response in this case, so check for a missing response before calling act().

Sources & review

Last reviewed by the ARC Proxies team. Prices are read from the live catalog at build time.

  1. [1]Stagehand: Browser configuration (v4): local proxy options and limitations
  2. [2]Stagehand: Browser configuration (v3): localBrowserLaunchOptions.proxy
  3. [3]Apify (GitHub): proxy-chain: anonymizeProxy() and closeAnonymizedProxy()
  4. [4]Chromium: Proxy support in Chrome: credentials in proxy settings

Next

What people build with it

Use caseResidential proxies for AI agentsBring your own residential IPs to browser-use, Playwright, Crawl4AI, n8n and any MCP tool — one sticky session per task, billed per GB.Use caseProxies for web scraping and data collectionA second provider that just works — Scrapy, requests, Crawlee or Playwright, self-serve, no sales call, bandwidth that never expires.Use caseProxies for QA, app and localisation testingSee your own site the way a user in Tokyo, São Paulo or Munich sees it — currency, language, consent banner, CDN edge and all — before they report it.GlossaryHTTP CONNECTHTTP CONNECT is the HTTP request method a client sends to a proxy to ask for a tunnel to a host and port; once the proxy answers with a 2xx status it blindly forwards bytes in both directions, which is how HTTPS passes through an HTTP proxy with TLS intact end to end (RFC 9110, section 9.3.6).Use caseResidential proxies for AI agentsBring your own residential IPs to browser-use, Playwright, Crawl4AI, n8n and any MCP tool — one sticky session per task, billed per GB.Integrationbrowser-useGive a browser-use agent its own exit IP: ProxySettings on the Browser, one sticky session per task, country targeting, a pre-flight check and retries.IntegrationPlaywrightSet up an authenticated proxy in Playwright for Python and Node: launch and per-context proxies, sticky sessions, geo-targeting and handling proxy errors.GuideRotating vs sticky sessionsRotating proxies change exit IP per connection; sticky sessions hold one IP for a set time. How each works, when to use which, and how to set them up.

Get started

Get credentials and paste them in.

Sign up, buy a gigabyte, generate a username in the dashboard. Code WELCOME15 takes 15% off your first purchase; bandwidth never expires.