Testing Your Staging App on Real Devices Without Opening a Firewall Port
BrowserStack Local Testing uses an outbound-only tunnel to let cloud devices reach your staging app without firewall changes. Here's how it works, a CI setup, and where it falls short.
28 Feb 2026, 18:25 UTC

Your staging environment sits behind a VPN, and your QA lead wants the checkout flow verified on a real iPhone 15 and a Galaxy S24 before Friday's release. You could poke a hole in the firewall, deploy to a public URL, or beg IT for a temporary exception. Or you could use BrowserStack Local Testing, which solves this exact problem with an outbound-only tunnel: nothing in your network needs to accept inbound connections at all.
The useful takeaway: Local Testing is a small binary that dials out from your machine or CI runner to BrowserStack's servers, so cloud devices can reach your localhost or internal IPs through that tunnel. Your security posture doesn't change, and your tests run against the real staging build.
How the tunnel actually works
The BrowserStackLocal binary opens an outbound WebSocket connection to BrowserStack's infrastructure. Because the connection originates from inside your network, it passes through standard corporate firewalls and NAT without configuration. When a test session on a cloud device requests http://localhost:3000, BrowserStack routes that request back down the tunnel to the machine running the binary, which forwards it to your dev server and returns the response.
This means the binary is the bridge: the cloud device never talks to your network directly, and your internal hostnames resolve on the machine running the binary, not in the cloud. If your staging app lives at staging.internal.company.com, that name only needs to resolve on the runner.
A working setup in CI
The most common production use is running the binary as a background step in a pipeline. Here's a minimal example for a Linux CI runner with a Node.js test suite:
# Run on the CI runner (Linux x64). Requires network access to
# *.browserstack.com on outbound port 443.
# Download the binary (pin a version in real pipelines)
curl -o BrowserStackLocal \
https://www.browserstack.com/local-testing/downloads/BrowserStackLocal-linux-x64
chmod +x BrowserStackLocal
# Start the tunnel in the background; key comes from a CI secret
./BrowserStackLocal --key "$BROWSERSTACK_ACCESS_KEY" \
--local-identifier "ci-build-$BUILD_ID" --daemon start
# Confirm it's up before running tests
./BrowserStackLocal --key "$BROWSERSTACK_ACCESS_KEY" \
--local-identifier "ci-build-$BUILD_ID" --daemon stopThree things matter here. First, BROWSERSTACK_ACCESS_KEY should come from your CI secret store, never a committed file. Second, --local-identifier scopes the tunnel to a specific build, which prevents collisions when multiple pipelines run concurrently. Third, verify the binary prints a "Connected" message (or check the exit code of a status command) before letting tests run — a silent tunnel failure produces confusing "connection refused" errors in every test.
On the test side, your Selenium, Playwright, or Cypress configuration needs two capabilities set: browserstack.local: true and browserstack.localIdentifier matching the value above. Then a test hitting http://localhost:3000/checkout on a real cloud device will reach the dev server on your runner. Success is easy to confirm: the BrowserStack dashboard shows the session under the Local Testing tab, and your dev server's access log shows the incoming requests.
Scoping the tunnel's exposure
By default the tunnel will proxy any host the runner can reach. In a shared CI environment that's broader than you want. The binary supports restricting which hosts or folders it will serve, so a compromised or misbehaving test can't use the tunnel to reach your internal wiki or production database admin panel. Treat the runner as a trust boundary: run it on a dedicated worker or container that can only reach the staging app, and you get defense in depth even before touching binary flags.
Where Local Testing falls short
The tunnel adds a network hop through BrowserStack's infrastructure, plus encryption overhead. For functional tests — does the button work, does the form submit — the added latency is typically sub-second and irrelevant. But don't use it for anything where timing matters. Performance budgets, load tests, and Lighthouse-style measurements through the tunnel will not reflect production conditions, and large file uploads can slow noticeably. Run those against a publicly reachable environment instead.
Two operational limits also bite teams as they scale. Accounts cap concurrent tunnels (lower tiers around five), so a heavily parallelized suite needs either fewer tunnels with more tests per tunnel, or a plan upgrade. And the binary is OS- and architecture-specific — an outdated or wrong-arch binary fails to connect, sometimes with unhelpful errors. Install it via the npm package or Homebrew where possible so updates ride along with your normal dependency maintenance rather than becoming a manual chore.
Try it in ten minutes
Download the binary for your OS, start any local server (python3 -m http.server 8000 works fine), run ./BrowserStackLocal --key YOUR_ACCESS_KEY, and look for the "Connected" message. Then open a manual session on any BrowserStack device and browse to http://localhost:8000. If you see your directory listing on a real phone halfway around the world, the tunnel works — and your firewall never noticed a thing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.