Testing Internal Sites on BrowserStack: How Local Tunneling Actually Works
BrowserStack Local tunnels let cloud browsers reach your localhost or internal staging. Here's how to set it up reliably in CI, handle self-signed certs, and avoid the latency and parallelism traps that bite teams at scale.
01 Jul 2025, 07:49 UTC

The Problem: Your Staging Site Isn't Public
You've got a feature branch deployed to http://localhost:3000 or an internal staging environment behind a corporate VPN. You need to run cross-browser tests on real devices in BrowserStack's cloud. The catch: those cloud browsers can't reach your private network.
BrowserStack Local creates an encrypted tunnel from your machine (or CI runner) to their infrastructure, letting remote browsers resolve and load your internal URLs as if they were local. This isn't a VPN—it's a targeted reverse proxy that only carries traffic for the specific session.
How the Tunnel Establishes Connectivity
When you start the BrowserStack Local binary, it opens a persistent WebSocket connection to BrowserStack's edge. The cloud side assigns a unique tunnel identifier. Any browser session started with the browserstack.local capability flag routes its DNS resolution and HTTP traffic through that WebSocket back to your machine, where the binary forwards requests to the actual host.
Key distinction: the tunnel binds to localhost on your side by default. If your app listens on 127.0.0.1 only, it works. If it binds to 0.0.0.0 or a LAN IP, you may need the --local-identifier flag to route correctly across multiple tunnel instances.
CI/CD Integration: Programmatic Control Over the Binary
For automated pipelines, don't rely on a background binary you start manually. Use the @browserstack/local npm package (or language equivalents) to spin up the tunnel programmatically within the job:
const Local = require('@browserstack/local');
const bsLocal = new Local();
await bsLocal.start({
key: process.env.BROWSERSTACK_ACCESS_KEY,
localIdentifier: process.env.BUILD_NUMBER,
forceLocal: true, // route all traffic through tunnel
onlyAutomate: true // skip Live dashboard traffic
});
// run your Playwright/Selenium tests here
await bsLocal.stop();
Run this on your CI agent (GitHub Actions, GitLab Runner, Jenkins node) where the application under test is reachable. The localIdentifier lets you run concurrent tunnels per PR build without collisions. Set forceLocal: true when your internal site references external CDN assets you don't want proxied—otherwise those requests also traverse the tunnel, adding latency.
Handling Self-Signed Certificates and Host Overrides
Internal staging often uses self-signed TLS. BrowserStack Local accepts a --proxy-host/--proxy-port for corporate proxies, but for certificate errors you have two paths:
- Browser capability: Pass
acceptInsecureCerts: truein your WebDriver/Playwright capabilities. Quick, but lowers security posture for the whole session. - Tunnel-level: Start the binary with
--force-proxyand configure your app to trust BrowserStack's CA, or use--local-proxy-user/--local-proxy-passif your proxy handles TLS termination.
For host header routing (e.g., staging.internal resolves to 127.0.0.1 only inside your network), add --host-file pointing to a hosts-formatted file on the tunnel machine. The binary will resolve those names locally before forwarding.
Worked Example: Playwright Test Against Localhost
Assume a Next.js dev server on http://localhost:3000. You want to run a Playwright test on Chrome and Safari via BrowserStack Automate.
- Start tunnel on your laptop (or CI):
./BrowserStackLocal --key $BROWSERSTACK_ACCESS_KEY \ --local-identifier my-feature-branch \ --force-local - Playwright config (
playwright.config.ts):export default defineConfig({ projects: [ { name: 'bs-chrome', use: { connectOptions: { wsEndpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify({ browser: 'chrome', browser_version: 'latest', os: 'Windows 11', 'browserstack.local': true, 'browserstack.localIdentifier': 'my-feature-branch', 'bstack:options': { userName: process.env.BS_USER, accessKey: process.env.BS_KEY } }))}` } } } ] }); - Run:
npx playwright test --project=bs-chrome. The browser in BrowserStack's Windows 11 VM loadshttp://localhost:3000through your tunnel.
Verify it worked: check the BrowserStack Automate dashboard for the session. The network log should show requests to localhost:3000 with 200 responses. If you see DNS errors, confirm the tunnel binary logs Connected to BrowserStack and that your dev server binds to 0.0.0.0 (not just 127.0.0.1) when running in containers.
Trade-offs You'll Hit in Practice
- Latency: Every asset request round-trips from cloud browser → tunnel → your machine → app. A 50 ms local request becomes 150–300 ms. Disable
forceLocalfor static assets hosted on public CDNs. - Stability: Tunnel drops if your laptop sleeps, network switches, or firewall kills idle WebSockets. In CI, wrap tunnel start/stop in retries and set
--daemonmode with a health-check endpoint. - Parallelism: Each
localIdentifiercounts as one tunnel slot. Most plans allow 1–5 concurrent tunnels. If you spin 10 PR builds simultaneously, they'll queue or fail. Use a shared tunnel pool (one per CI runner) with unique identifiers per job.
Quick Verification Checklist
- Run
./BrowserStackLocal --key $KEY --local-identifier testand confirm "Connected" in stdout. - Open BrowserStack Live, start a session with the same
localIdentifier, navigate to your internal URL. - In Automate, run a one-line Selenium script:
driver.get('http://localhost:3000')withbrowserstack.local: truecapability. Assert page title.
If all three succeed, the tunnel path is solid. Document the localIdentifier naming convention for your team—collisions are the most common cause of "works locally, fails in CI" reports.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.