Skip to main content
SeleniumDecoded

Cloud Testing Platforms

Run Selenium on BrowserStack, Sauce Labs, LambdaTest and similar services: the vendor-prefixed capabilities, tunnels to private environments, marking results, cost control, and when a self-hosted Grid is cheaper.

Selenium 4 Stable Updated 9 Sept 2026 · Verified against Selenium 4.48.0

Cloud platforms are Selenium Grids someone else runs, with real devices, every browser version back a decade, video, logs and a dashboard. They exist because Selenium is a W3C protocol: your tests send the same commands to hub.browserstack.com as to localhost:4444. This lesson covers what changes (capabilities, networking, result reporting) and how to decide between cloud and self-hosted.

What You Get, and What It Costs

BenefitDetail
Browser and OS matrixSafari on real macOS, old Chrome and Firefox builds, Windows and Linux, real iOS and Android via Appium
Zero infrastructureNo Grid to patch, no images to build, no autoscaling to tune
EvidenceVideo, screenshots, console and network logs per session, in a dashboard
TunnelsTest apps on localhost or behind a firewall through a vendor agent

Costs: per-parallel-session pricing that grows with suite size, network latency of tens of milliseconds per command (a 500-command test gains 10 to 30 seconds), vendor lock-in through capability formats and SDKs, and data leaving your network.

Vendor-Prefixed Capabilities

Selenium 4 speaks W3C, so vendor settings must sit under a prefixed key. The three main platforms:

VendorEndpointOptions key
BrowserStackhttps://hub-cloud.browserstack.com/wd/hubbstack:options
Sauce Labshttps://ondemand.<region>.saucelabs.com/wd/hubsauce:options
LambdaTest (TestMu AI)https://hub.lambdatest.com/wd/hubLT:Options

Credentials go in environment variables, never in code.

Connect to a cloud grid (BrowserStack shown; others differ only in key names)
Selenium 4 Stable
String user = System.getenv("BROWSERSTACK_USERNAME");
String key = System.getenv("BROWSERSTACK_ACCESS_KEY");
ChromeOptions options = new ChromeOptions();
options.setBrowserVersion("latest");
options.setPlatformName("Windows 11");
Map<String, Object> bstack = new HashMap<>();
bstack.put("projectName", "Storefront");
bstack.put("buildName", System.getenv().getOrDefault("CI_BUILD", "local"));
bstack.put("sessionName", "checkout applies coupon");
bstack.put("local", "true"); // route through the tunnel
bstack.put("debug", "true"); // screenshots per step
bstack.put("networkLogs", "true");
bstack.put("seleniumVersion", "4.48.0");
options.setCapability("bstack:options", bstack);
URL url = new URL("https://" + user + ":" + key + "@hub-cloud.browserstack.com/wd/hub");
RemoteWebDriver driver = new RemoteWebDriver(url, options);
driver.setFileDetector(new LocalFileDetector());
import os
options = webdriver.ChromeOptions()
options.browser_version = "latest"
options.platform_name = "Windows 11"
options.set_capability("bstack:options", {
"projectName": "Storefront",
"buildName": os.getenv("CI_BUILD", "local"),
"sessionName": "checkout applies coupon",
"local": "true",
"debug": "true",
"networkLogs": "true",
"seleniumVersion": "4.48.0",
})
url = f"https://{os.environ['BROWSERSTACK_USERNAME']}:{os.environ['BROWSERSTACK_ACCESS_KEY']}@hub-cloud.browserstack.com/wd/hub"
driver = webdriver.Remote(command_executor=url, options=options)
driver.file_detector = LocalFileDetector()
const options = new chrome.Options()
.setBrowserVersion('latest')
.setPlatformName('Windows 11')
.set('bstack:options', {
projectName: 'Storefront',
buildName: process.env.CI_BUILD ?? 'local',
sessionName: 'checkout applies coupon',
local: 'true',
debug: 'true',
networkLogs: 'true',
seleniumVersion: '4.48.0',
});
const url = `https://${process.env.BROWSERSTACK_USERNAME}:${process.env.BROWSERSTACK_ACCESS_KEY}@hub-cloud.browserstack.com/wd/hub`;
const driver = await new Builder().usingServer(url).forBrowser('chrome').setChromeOptions(options).build();
var options = new ChromeOptions { BrowserVersion = "latest", PlatformName = "Windows 11" };
options.AddAdditionalOption("bstack:options", new Dictionary<string, object>
{
["projectName"] = "Storefront",
["buildName"] = Environment.GetEnvironmentVariable("CI_BUILD") ?? "local",
["sessionName"] = "checkout applies coupon",
["local"] = "true",
["debug"] = "true",
["networkLogs"] = "true",
["seleniumVersion"] = "4.48.0",
});
var user = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME");
var key = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY");
var driver = new RemoteWebDriver(new Uri($"https://{user}:{key}@hub-cloud.browserstack.com/wd/hub"), options);
driver.FileDetector = new LocalFileDetector();

Sauce Labs uses sauce:options with build, name, tunnelName; LambdaTest uses LT:Options with build, name, tunnel. Each vendor’s capability generator produces the exact map for your browser and OS combination.

Tunnels to Private Environments

Cloud browsers cannot reach localhost or a staging server behind your firewall. Each vendor ships a tunnel binary (BrowserStack Local, Sauce Connect, LambdaTest Tunnel) that opens an outbound connection from your network; browsers route through it when the capability (local, tunnelName, tunnel) is set.

In CI, start the tunnel before tests and stop it after; vendor GitHub Actions and Jenkins plugins do this. Name tunnels per job (localIdentifier) so parallel pipelines do not share one.

Marking Results

The cloud dashboard does not know whether your assertions passed. Tell it in the teardown hook through the vendor’s JavaScript executor command or REST API.

Report pass or fail to the dashboard
Selenium 4 Stable
// BrowserStack: executor command
String status = failed ? "failed" : "passed";
String reason = failed ? cause.getMessage().replace("\"", "'") : "";
((JavascriptExecutor) driver).executeScript(
"browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\":\"" + status + "\", \"reason\": \"" + reason + "\"}}");
// Sauce Labs
((JavascriptExecutor) driver).executeScript("sauce:job-result=" + status);
// LambdaTest
((JavascriptExecutor) driver).executeScript("lambda-status=" + status);
import json
status = "failed" if failed else "passed"
# BrowserStack
driver.execute_script("browserstack_executor: " + json.dumps({
"action": "setSessionStatus",
"arguments": {"status": status, "reason": str(exc)[:250] if failed else ""},
}))
# Sauce Labs
driver.execute_script(f"sauce:job-result={status}")
# LambdaTest
driver.execute_script(f"lambda-status={status}")
const status = failed ? 'failed' : 'passed';
// BrowserStack
await driver.executeScript(`browserstack_executor: ${JSON.stringify({
action: 'setSessionStatus', arguments: { status, reason: failed ? err.message.slice(0, 250) : '' },
})}`);
// Sauce Labs
await driver.executeScript(`sauce:job-result=${status}`);
// LambdaTest
await driver.executeScript(`lambda-status=${status}`);
var status = failed ? "failed" : "passed";
var js = (IJavaScriptExecutor)driver;
// BrowserStack
js.ExecuteScript("browserstack_executor: " + JsonSerializer.Serialize(new
{
action = "setSessionStatus",
arguments = new { status, reason = failed ? message[..Math.Min(250, message.Length)] : "" },
}));
// Sauce Labs
js.ExecuteScript($"sauce:job-result={status}");
// LambdaTest
js.ExecuteScript($"lambda-status={status}");

Also set sessionName (or name) from the test name and buildName from the CI build id so the dashboard groups sessions the way your pipeline does.

Cloud-Specific Behaviour to Plan For

  • Latency: batch where possible. Read several values with one executeScript instead of several getText calls. Use pageLoadStrategy = eager.
  • Uploads need LocalFileDetector; downloads use vendor-specific APIs or are best asserted via the app’s UI.
  • Timeouts: vendors kill idle sessions (typically 90 seconds without a command) and long sessions (30 minutes to 2 hours). Keep tests short.
  • Parallel limits: exceeding your plan queues sessions; align runner parallelism with the plan.
  • Mobile: real devices use Appium capabilities (appium: prefix), same driver classes.
  • BiDi: supported on major vendors for Chromium and Firefox; check the vendor’s Selenium version support before relying on it.

Cloud vs Self-Hosted Grid

FactorCloudSelf-hosted (Docker or Kubernetes)
Time to first testMinutesHours to days
Browser matrixEverything, including Safari and real devicesChrome, Edge, Firefox on Linux; Safari needs a Mac
Cost at scaleLinear per parallel session; expensive above roughly 20 to 50 parallelsMostly fixed after setup; cheap per additional session
LatencyTens of ms per commandSingle-digit ms in the same network
Data localityTraffic leaves your network (tunnel encrypts it)Stays internal
MaintenanceVendor’sYours: upgrades, images, autoscaling, video storage

A common and sensible split: self-hosted Grid for the bulk Chrome runs on every pull request, cloud for the nightly cross-browser and real-device matrix.

Summary

  • Cloud platforms are remote Grids; only the URL, credentials and vendor-prefixed options change.
  • Use tunnels for private environments, LocalFileDetector for uploads, and mark results from your teardown hook.
  • Design for latency and session limits: fewer commands, shorter tests, parallelism matched to the plan.
  • Choose cloud for breadth and speed to start, self-hosted for volume and locality, or both.

Related lessons