Cross-browser tests on a single laptop work fine for dozens of tests. At hundreds, your machine becomes the bottleneck — the browsers compete for CPU, the suite slows to a crawl, and CI runners can't easily replicate "Firefox 128 on Ubuntu 22.04." Selenium Grid is the open-source answer: a small server that distributes WebDriver sessions to remote browser nodes. You point your tests at the Grid's URL with RemoteWebDriver; the Grid hands the work to whichever node has matching capabilities. This lesson runs a Grid locally as a JAR, then via Docker Compose (the production-friendly path), and shows the test-side change to make your existing suite Grid-aware.
What Grid actually is
Selenium Grid 4 has two roles:
Hub — receives test requests, queues them, dispatches them to nodes.
Node — a machine (or container) with browsers installed; runs the requested sessions.
In standalone mode (the easiest local setup), one process is both hub and node. In distributed mode (production), the hub and nodes run on different machines and the hub routes to whichever node can serve a given browser/version/OS combination.
The thing that hasn't changed across Selenium versions: the test code. RemoteWebDriver is the only difference. Same findElement, same WebDriverWait, same page objects.
The Grid architecture
Test codeRemoteWebDriver → http://hub:4444
HubRoutes by capabilities (browser, version…
Chrome nodeRuns Chrome sessions
Firefox nodeRuns Firefox sessions
A test asks for "any chrome session" and the hub assigns one. When the test finishes, the session ends; the node is free for the next request. With parallel="methods" in TestNG, dozens of test methods can hold dozens of sessions simultaneously — limited only by node capacity.
A small log scrolls. The Grid is now serving on http://localhost:4444. Open the dashboard: http://localhost:4444/ui. You'll see one node, with Chrome and Firefox capabilities (assuming both are installed on your machine).
Pointing tests at the Grid
Update BaseTest's factory to use RemoteWebDriver when a grid URL is provided:
import org.openqa.selenium.remote.RemoteWebDriver;import java.net.URI;private WebDriver createDriver(String browser, boolean headless) { String gridUrl = System.getProperty("grid.url", ""); switch (browser.toLowerCase()) { case "chrome": { ChromeOptions o = new ChromeOptions(); if (headless) o.addArguments("--headless=new", "--no-sandbox", "--disable-dev-shm-usage"); o.addArguments("--window-size=1920,1080"); if (!gridUrl.isEmpty()) { return new RemoteWebDriver(URI.create(gridUrl).toURL(), o); } WebDriverManager.chromedriver().setup(); return new ChromeDriver(o); } // firefox, edge cases similar }}
Two modes for the same test:
# Local — drives a Chrome on this machinemvn test -Dbrowser=chrome# Grid — drives a Chrome session on the Grid hubmvn test -Dbrowser=chrome -Dgrid.url=http://localhost:4444
The test code, page objects, and @DataProvider are untouched. Only the driver source changes.
Docker Compose Grid — the production-friendly setup
Running browsers locally couples your suite to the laptop's installed browsers. Docker Compose is the cleaner path: each browser type is a container, hub is a container, everything reproducible across machines.
The hub is on http://localhost:4444. Open http://localhost:4444/ui — the dashboard shows one Chrome node and one Firefox node, each willing to handle four sessions concurrently.
Scaling nodes — one command
The whole point of Grid: scale by adding nodes, not browsers-per-laptop. Compose makes it trivial:
# Run five Chrome containers in paralleldocker compose up -d --scale chrome=5
Now the dashboard shows five Chrome nodes, each able to handle four sessions — 20 concurrent Chrome sessions. Pair with parallel="methods" thread-count="20" in testng.xml and your 200-test suite finishes in roughly the time of 10 sequential tests.
Watching the Grid in action
The dashboard at http://localhost:4444/ui is the most underused page in the Selenium ecosystem. While a parallel suite runs, refresh it — you'll see live sessions, node load, queue depth. Diagnostic gold:
Queue stuck at high count → not enough nodes. Scale up.
One node hot, others idle → capability mismatch. Most tests want Chrome and you have only one Chrome node.
Sessions starting and immediately failing → tests can't reach the hub URL. Network/Docker config issue.
Treat the dashboard as a first-class debugging tool when grid-based suites flake.
Cloud Grid — same protocol, more breadth
Local Grid covers the browsers you can install. For Safari on macOS, IE 11, Chrome on Android, Firefox on Windows Server, you need a cloud:
// BrowserStack — point RemoteWebDriver at their hub with credentials in capabilitiesDesiredCapabilities caps = new DesiredCapabilities();caps.setCapability("browserName", "Safari");caps.setCapability("os", "OS X");caps.setCapability("os_version", "Sonoma");caps.setCapability("browserstack.user", System.getenv("BS_USER"));caps.setCapability("browserstack.key", System.getenv("BS_KEY"));WebDriver driver = new RemoteWebDriver( URI.create("https://hub-cloud.browserstack.com/wd/hub").toURL(), caps);
Sauce Labs, LambdaTest, Testingbot all follow the same pattern with their own hub URLs and capability conventions. The test code is identical to local Grid — that's the value of the WebDriver protocol being a real spec.
A complete Grid-driven test
package com.mycompany.tests.tests;import com.mycompany.tests.base.BaseTest;import com.mycompany.tests.pages.InventoryPage;import com.mycompany.tests.pages.LoginPage;import org.testng.Assert;import org.testng.annotations.Test;public class GridDrivenLoginTest extends BaseTest { @Test public void shouldLogInOnGrid() { InventoryPage inventory = new LoginPage(driver) .navigateTo() .loginAs("standard_user", "secret_sauce"); Assert.assertEquals(inventory.productCount(), 6, "Same six products regardless of which Grid node ran the test"); }}
Same test, different runtime:
# Local Chromemvn test -Dtest=GridDrivenLoginTest# Local Grid (Docker Compose) Chromedocker compose up -dmvn test -Dtest=GridDrivenLoginTest -Dgrid.url=http://localhost:4444# BrowserStack — Safari on macOS Sonomamvn test -Dtest=GridDrivenLoginTest \ -Dgrid.url=https://hub-cloud.browserstack.com/wd/hub \ -Dbrowser=safari -Dremote.os=mac
One test class, three runtimes — the Grid abstraction is what makes this possible.
When Grid earns its place
Three honest cases:
Suite wall-clock matters and a single laptop can't keep up. Grid + parallel methods is the standard answer.
You need cross-browser/OS coverage you can't install locally. Cloud Grid is the only practical option.
Multiple engineers and CI share browser instances. A shared internal Grid means everyone's tests run consistently, with the same browser versions.
For a 30-test smoke suite running on Chrome only, a single laptop is fine — Grid would be overhead. Add it when the suite's runtime starts to bite.
Comparison with Playwright and Cypress
// Playwright — no Grid concept// Browsers are downloaded locally per-project. To scale, run multiple Playwright instances// across CI runners. There's no centralised "browser farm" needed.// Cypress — Cypress Cloud (paid) handles parallelism// You can also run Cypress in Docker, but the parallelism model is built around CI runner// fan-out, not a hub-and-node Grid.
Selenium Grid is one of Selenium's most distinctive features. Playwright sidesteps the problem by making browsers cheap to install per project. Cypress went the cloud-only route. Grid is the right answer when the workflow is "one suite, many browser/OS targets, scale-out across machines" — exactly the enterprise scenario Selenium has owned for two decades.
Forgetting shm_size: 2gb on Chrome containers. The default /dev/shm is small (often 64MB), and Chrome dies mid-session with chrome failed to start. The 2GB allocation in the Compose file solves it. Same for Firefox if you see similar crashes.
Pointing tests at localhost:4444 from inside a Docker container. When tests run inside the same Compose network as the hub, the URL is http://hub:4444 (the service name), not localhost. localhost from a container points at the container itself, not the host. Distinguish "tests on host driving Grid in containers" from "tests in container driving Grid in same network" — the URLs differ.
Running cross-browser parallel suites without ThreadLocal<WebDriver>. With parallel="methods", dozens of threads create drivers concurrently. A shared protected WebDriver driver; is fine when TestNG instantiates a fresh test class per method (the default), but if you've made the field static or share it across instances, threads stomp each other. Chapter 8 covers the ThreadLocal pattern in detail.
🎯 Practice task
Spin up a Grid and run real tests against it. 40–50 minutes.
Install Docker Desktop (if you don't have it). Confirm docker --version and docker compose version both work.
Create docker-compose.yml from this lesson. Run docker compose up -d. Open http://localhost:4444/ui — confirm you see one Chrome node and one Firefox node.
Update BaseTest to use RemoteWebDriver when -Dgrid.url=... is set. Run:
mvn test -Dtest=LoginPomTest -Dgrid.url=http://localhost:4444 -Dbrowser=chrome
Watch the dashboard while the test runs — a session appears, runs, disappears.
Scale Chrome. Run docker compose up -d --scale chrome=3. Refresh the dashboard. Run your full suite with parallel="methods" thread-count="6" in a Grid-aware testng.xml. The dashboard shows up to six concurrent sessions across three Chrome nodes.
Force a queue. Set thread-count="20" but keep only one Chrome node. Run a long-ish suite. The dashboard shows queued requests waiting for a free node — the visualisation of overcommitment.
Compare local vs Grid runtime. Time the same 20-test suite running locally on Chrome (mvn test) and against a 3-node Grid (-Dgrid.url=http://localhost:4444 thread-count=10). The Grid version should be roughly 3× faster — that's the value Grid delivers.
Stretch — cloud Grid. Sign up for BrowserStack's free trial. Get a username and access key. Wire a RemoteWebDriver at their hub URL with credentials in capabilities. Run one of your tests against Safari on macOS — a browser literally not present on a Linux box. The free tier gives you ~100 minutes total, enough to feel the workflow.
Chapter 7 is done. You can now drive Selenium tests from any data source, against any local or remote browser, in parallel, with no test-class changes. Chapter 8 takes the final step: getting all of this running on Jenkins and GitHub Actions so the suite runs on every commit, every PR, and every nightly cron — without anyone touching a laptop.
// tip to track lessons you complete and pick up where you left off across devices.