PRACTICAL GUIDE / selenium grid tutorial

Selenium Grid Tutorial: Run Tests Across Browsers

Selenium Grid tutorial explaining architecture, setup, remote WebDriver, browser capabilities, parallel execution, Docker, CI, and debugging tips.

By The Testing AcademyUpdated July 10, 202610 min read
All field guides
In this guide10 sections
  1. Decide what the Grid must provide
  2. Understand the remote session path
  3. Run a pinned container topology
  4. Create remote drivers from explicit options
  5. Make each parallel test own its driver
  6. Write a remote-safe browser test
  7. Transfer files with remote execution in mind
  8. Tune concurrency from measured capacity
  9. Diagnose Grid failures by layer
  10. Operate Grid as CI infrastructure

What you will learn

  • Decide what the Grid must provide
  • Understand the remote session path
  • Run a pinned container topology
  • Create remote drivers from explicit options

Twenty stable local tests can become sixty confusing failures when moved to remote browsers. The Grid is often blamed, but the actual causes are shared test accounts, files assumed to exist on the test runner, browser capabilities that never matched the node, and timeouts tuned for localhost latency. Selenium Grid exposes architecture problems that sequential local execution hid.

This guide builds a small Java and TestNG suite for a document portal, runs it against containerized Grid nodes, and adds the diagnostics needed to distinguish an application failure from an infrastructure failure.

Decide what the Grid must provide

Start with a coverage matrix, not a node count. Record browser, operating system, viewport, locale, and any special requirements such as downloads or certificates. A Linux Chrome node does not prove a Windows-specific integration, and three identical nodes add capacity but no new browser coverage.

Separate two goals:

  • Concurrency shortens feedback by running independent tests at the same time.
  • Distribution provides browsers or operating systems unavailable on the runner.

They have different costs. If the only goal is faster Chromium checks, several local headless processes may be simpler. Grid earns its operational overhead when the suite needs shared capacity, remote environments, or a controlled cross-browser service.

Understand the remote session path

A test creates RemoteWebDriver against the Grid endpoint and sends desired capabilities. Grid matches the request to an available slot, creates a browser session on a node, then routes WebDriver commands to that session.

In a distributed deployment, components handle routing, session queuing, slot discovery, and event communication. A small team does not need to operate each component separately on day one. Start with a standalone server or a hub plus nodes, prove the suite, then split components only when scale or resilience justifies it.

The Grid status endpoint should be part of health checks. An HTTP listener being open does not prove a matching browser slot is ready. Inspect the Grid UI and status response when sessions remain queued.

Grid is a privileged control plane for real browsers. Do not expose port 4444 directly to the public internet. Place it on a private network, require authentication at an approved gateway where applicable, and restrict which CI identities can create sessions. Browser sessions can reach internal applications and may carry test credentials, so network placement is part of the threat model.

Separate the test-to-Grid route from the node-to-application route when debugging. The runner may reach the hub while the browser container cannot resolve the application hostname. A health check executed on the runner does not prove node connectivity. Add a small diagnostic session that opens the application health URL from the browser network.

Run a pinned container topology

The following Compose file creates a hub and two Chrome nodes. The image tag comes from an environment variable so CI can pin a reviewed Selenium image rather than silently pulling a moving tag.

YAML
# docker-compose.grid.yml
services:
  selenium-hub:
    image: selenium/hub:${SELENIUM_TAG}
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"

  chrome-1:
    image: selenium/node-chrome:${SELENIUM_TAG}
    shm_size: 2gb
    depends_on: [selenium-hub]
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
      SE_NODE_MAX_SESSIONS: "1"

  chrome-2:
    image: selenium/node-chrome:${SELENIUM_TAG}
    shm_size: 2gb
    depends_on: [selenium-hub]
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
      SE_NODE_MAX_SESSIONS: "1"
Shell
export SELENIUM_TAG='<reviewed-image-tag>'
docker compose -f docker-compose.grid.yml up -d
curl --fail http://localhost:4444/status

Use a concrete tag or digest in the real CI variable. One session per browser container is a conservative starting point. Increasing SE_NODE_MAX_SESSIONS beyond the node's CPU and memory capacity can make every session slower and less reliable. Shared memory sizing is important for containerized browsers that render complex pages.

Create remote drivers from explicit options

Keep the Grid URL external to the code. Build browser options in one factory and fail if an unsupported browser is requested.

Java
package testinfra;

import java.net.MalformedURLException;
import java.net.URI;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.remote.RemoteWebDriver;

public final class DriverFactory {
    private DriverFactory() {}

    public static WebDriver create(String browser) {
        var gridUrl = System.getProperty("grid.url", "http://localhost:4444");

        try {
            return switch (browser.toLowerCase()) {
                case "chrome" -> new RemoteWebDriver(
                    URI.create(gridUrl).toURL(),
                    new ChromeOptions().addArguments("--window-size=1440,900")
                );
                case "firefox" -> new RemoteWebDriver(
                    URI.create(gridUrl).toURL(),
                    new FirefoxOptions()
                );
                default -> throw new IllegalArgumentException(
                    "Unsupported browser: " + browser
                );
            };
        } catch (MalformedURLException error) {
            throw new IllegalArgumentException("Invalid Grid URL: " + gridUrl, error);
        }
    }
}

Request only capabilities the suite actually needs. Adding platform names or browser versions that no node advertises leaves sessions queued until timeout. Read the Grid console or status payload to compare requested and available capabilities.

Make each parallel test own its driver

WebDriver instances are not shared across threads. A ThreadLocal wrapper can associate one remote session with each TestNG worker, but lifecycle still needs disciplined setup and teardown.

Java
package testinfra;

import org.openqa.selenium.WebDriver;

public final class DriverSession {
    private static final ThreadLocal<WebDriver> CURRENT = new ThreadLocal<>();

    public static void start(String browser) {
        if (CURRENT.get() != null) {
            throw new IllegalStateException("Driver already started");
        }
        CURRENT.set(DriverFactory.create(browser));
    }

    public static WebDriver get() {
        var driver = CURRENT.get();
        if (driver == null) throw new IllegalStateException("No driver for thread");
        return driver;
    }

    public static void stop() {
        var driver = CURRENT.get();
        try {
            if (driver != null) driver.quit();
        } finally {
            CURRENT.remove();
        }
    }
}

Always call quit, not only close, so Grid releases the slot. The remove call prevents a reused TestNG thread from retaining a dead session reference.

Write a remote-safe browser test

The document portal test creates a unique folder by API, then verifies it appears in the UI. It never assumes another test has logged in or prepared records.

Java
import static org.testng.Assert.assertTrue;
import java.time.Duration;
import java.util.UUID;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
import testinfra.DriverSession;

public class FolderTest {
    private String folderName;

    @BeforeMethod
    @Parameters("browser")
    public void setUp(String browser) {
        folderName = "grid-" + UUID.randomUUID();
        TestDataApi.createFolder(folderName);
        DriverSession.start(browser);
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown() {
        DriverSession.stop();
        TestDataApi.deleteFolder(folderName);
    }

    @Test
    public void showsCreatedFolder() {
        var driver = DriverSession.get();
        driver.get(System.getProperty("app.url") + "/folders");

        var folder = new WebDriverWait(driver, Duration.ofSeconds(15))
            .until(ExpectedConditions.visibilityOfElementLocated(
                By.xpath("//a[normalize-space()=" + quoteForXPath(folderName) + "]")
            ));

        assertTrue(folder.isDisplayed());
    }
}

TestDataApi and quoteForXPath are project utilities that must be implemented and tested. Prefer a test ID carrying the folder's stable record ID if the application can expose one. Constructing XPath with untrusted text is fragile, especially when values contain both quote types.

Transfer files with remote execution in mind

A path such as /home/runner/fixtures/report.pdf exists on the CI runner, not inside the remote browser node. Selenium's remote file upload support transfers a local file when sendKeys targets an input of type file, provided the driver uses an appropriate file detector.

Java
import java.nio.file.Path;
import org.openqa.selenium.remote.LocalFileDetector;
import org.openqa.selenium.remote.RemoteWebDriver;

var driver = (RemoteWebDriver) DriverSession.get();
driver.setFileDetector(new LocalFileDetector());

Path fixture = Path.of("src", "test", "resources", "sample.pdf")
    .toAbsolutePath();
driver.findElement(By.cssSelector("input[type='file']"))
    .sendKeys(fixture.toString());

Downloads have the opposite challenge: the file is created on the node. Decide whether the test needs browser-download retrieval, a mounted artifact path, or an API verification of the generated document. Do not assert against a runner directory that the node cannot write.

Tune concurrency from measured capacity

TestNG can run methods or classes in parallel, but the thread count should not exceed the Grid's matching slots. It should also respect application and database capacity.

XML
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="grid-smoke" parallel="methods" thread-count="2">
  <parameter name="browser" value="chrome"/>
  <test name="document portal">
    <classes>
      <class name="FolderTest"/>
    </classes>
  </test>
</suite>

Two slots do not guarantee that two tests are safe. Shared rate limits, mutable accounts, fixed download names, and cleanup by broad prefixes all create collisions. Increase concurrency one step at a time while watching session queue time, node CPU, memory, browser crashes, and backend error rates.

Queue time and test execution time should be measured separately. A ten-minute job may contain two minutes of browser work and eight minutes waiting for slots. Adding nodes can help the latter, while optimizing locators cannot. If execution itself becomes slower as sessions increase, the bottleneck may be the application, database, network, or host CPU.

Apply back pressure instead of sending unbounded session requests. Match TestNG thread count and CI matrix fan-out to the available slot pool. Multiple repositories can share a Grid only if an admission policy prevents one large suite from starving release-critical work.

Diagnose Grid failures by layer

Capture the remote session ID and capabilities as soon as the driver starts. On failure, save a screenshot, browser console output where supported, current URL, test data ID, and Grid node information. Correlate timestamps with hub and node logs.

Classify failures in this order:

  1. Session creation: no matching slot, invalid capability, image startup, or exhausted capacity.
  2. Transport: proxy, DNS, TLS, or connection interruption between test and Grid.
  3. Browser: crash, resource exhaustion, profile issue, or unsupported option.
  4. Application: server error, unavailable dependency, or environment data.
  5. Test design: weak wait, local-file assumption, shared state, or brittle locator.

A longer WebDriver wait can help only the last two timing cases. It cannot create a matching node or repair a lost network route.

Retain hub and node logs with a common clock and include the session ID in test artifacts. Without synchronized timestamps, it is difficult to connect a client timeout to a node restart or browser crash. Track session creation latency, queue depth, active slots, failed session creation, and unexpected node loss as infrastructure signals.

Screenshots belong to the browser session, while container logs belong to the node. Capture both before teardown if possible. When a node disappears, screenshot collection may be impossible, so the Grid logs and last command become the primary evidence. Do not let artifact collection exceptions replace the original WebDriver error.

Maintain a minimal canary spec that opens a static page, asserts a heading, and quits. If the canary fails, application feature tests add noise. Run it after image updates and before a large compatibility job to confirm routing, browser startup, and basic command execution.

Operate Grid as CI infrastructure

CI should start the pinned topology, wait for a ready status, run the suite, and collect Grid logs even when tests fail. Tear the containers down in an always-run step so abandoned sessions do not consume the next job.

Use separate smoke and broad compatibility stages. Pull requests can run the highest-value flows on one primary browser. Scheduled or release pipelines can execute the approved browser matrix. This keeps feedback proportional to risk without maintaining idle capacity for every commit.

Set queue, session, and test timeouts so failures terminate predictably, but avoid one global timeout large enough to hide a hung node. Monitor queue depth and session creation duration over time. Grid is healthy when capacity matches demand, sessions are isolated, artifacts identify the executing node, and a remote failure can be assigned to the correct layer without a local rerun.

Upgrade images through a staged lane. Start the proposed hub and node images beside the current pool, run the canary and a representative browser suite, then promote them. Keep rollback simple by retaining the previous pinned tag until the new pool has completed real jobs.

Patch cadence must cover the operating system, browser, driver, and Grid server. A node image bundles several moving security and compatibility surfaces. Record the effective capabilities and image identity with every run so a regression can be compared against the exact environment that produced it.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 10, 2026 / Reviewed July 10, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Selenium documentation

    Selenium Project

    Canonical WebDriver, Grid, waits, element, and browser automation guidance.

  2. 02
    WebDriver standard

    W3C

    The browser automation protocol specification behind WebDriver implementations.

FAQ / QUICK ANSWERS

Questions testers ask

What is Selenium Grid?

Selenium Grid is Selenium infrastructure that lets tests run on remote browsers across different machines, containers, operating systems, or browser versions. Instead of each test starting a local browser, the test sends commands to a Grid endpoint that routes sessions to available nodes.

Do beginners need Selenium Grid?

Not immediately. Beginners should first write stable local tests. Grid becomes useful when you need parallel execution, cross browser coverage, shared infrastructure, or CI scale. Running unstable tests on Grid only makes failures faster and harder to diagnose.

Is Docker required for Selenium Grid?

No, but Docker is a common and practical way to run Selenium Grid because browser nodes are easier to start, stop, and reproduce. You can also run Grid directly on machines or use a cloud provider that manages the infrastructure.

How many parallel Selenium sessions should I run?

Run as many as your application, data, Grid resources, and CI machines can support reliably. More sessions are not always better. If the app backend, test data, or environment cannot handle concurrency, parallel tests will create false failures.

What causes Selenium Grid tests to fail?

Common causes include browser version mismatch, insufficient node capacity, network latency, weak waits, shared data collisions, and tests assuming local files or local machine state. Grid exposes design problems that local sequential runs can hide.