The WebDriver specification explicitly states that WebDriver implementations are not thread-safe. Two threads operating on the same WebDriver instance will issue conflicting commands — one thread clicks a button while another is still waiting for a page to load — producing NoSuchSessionException, StaleElementReferenceException, and race conditions that disappear the moment you add a debug log. Driver management is the part of your framework that decides when drivers are created, how they're isolated across threads, and when they're destroyed. Get this right and parallel execution "just works." Get it wrong and you have a suite that's faster in theory but broken in practice.
The three strategies
Strategy 1: One driver per test (sequential)
The simplest approach. A new driver for every test method, created in setup and destroyed in teardown.
public class BaseTest {
protected WebDriver driver;
@BeforeMethod
public void setUp() {
driver = DriverFactory.create(Config.browser());
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(Config.timeoutSeconds()));
}
@AfterMethod(alwaysRun = true)
public void tearDown() {
if (driver != null) {
driver.quit();
driver = null;
}
}
}alwaysRun = true is mandatory. Without it, a test failure before tearDown leaves a zombie browser process running — consuming memory and file handles until the CI agent runs out of resources.
This strategy is correct, safe, and predictable. For a 50-test suite running sequentially, the 2–4 second driver startup cost per test is acceptable. At 500 tests, that's 16–33 minutes of browser startup alone.
Strategy 2: ThreadLocal driver (parallel-safe)
Covered in the Singleton lesson — revisited here as the cornerstone of parallel driver management. ThreadLocal<WebDriver> gives each thread its own independent driver instance. Thread A's getDriver() returns Thread A's driver; Thread B's returns Thread B's. No sharing, no interference.
public class DriverManager {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
public static WebDriver getDriver() {
if (DRIVER.get() == null) {
DRIVER.set(DriverFactory.create(Config.browser()));
}
return DRIVER.get();
}
public static void quitDriver() {
WebDriver d = DRIVER.get();
if (d != null) {
d.quit();
DRIVER.remove(); // prevents memory leak in thread pools
}
}
}Enable TestNG parallelism in the suite XML:
<suite name="Regression" parallel="methods" thread-count="4">
<test name="All Tests">
<packages>
<package name="com.mycompany.tests"/>
</packages>
</test>
</suite>Four threads, four independent ChromeDriver instances, four times the throughput. The thread-count ceiling is your machine's available memory and CPU cores — a browser process typically consumes 200–400 MB RAM. On CI, 4–8 threads per agent is a common sweet spot.
Strategy 3: Remote WebDriver / Selenium Grid (distributed)
When parallel threads on one machine aren't enough, distribute tests across a Grid of nodes. Each node runs a browser; the Grid hub routes requests from multiple test threads across multiple machines.
// Remote driver — connects to Grid hub
public static WebDriver createRemote(String browser) {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName(browser);
try {
return new RemoteWebDriver(new URL(Config.gridUrl()), caps);
} catch (MalformedURLException e) {
throw new RuntimeException("Invalid Grid URL: " + Config.gridUrl(), e);
}
}Cloud providers (BrowserStack, Sauce Labs, LambdaTest) are Selenium Grid equivalents with browser/OS combination matrices and built-in video recording:
// BrowserStack remote driver
ChromeOptions opts = new ChromeOptions();
HashMap<String, Object> bsOptions = new HashMap<>();
bsOptions.put("os", "Windows");
bsOptions.put("osVersion", "11");
bsOptions.put("buildName", System.getenv("BUILD_ID"));
opts.setCapability("bstack:options", bsOptions);
return new RemoteWebDriver(new URL(Config.bstackUrl()), opts);Driver management strategies — when to use each
Per-test driver
New driver created for every test method
Perfect isolation — zero shared state
Safe for sequential and parallel execution
2–4 sec startup cost per test
Best for: suites under 100 tests
ThreadLocal driver
One driver per thread, reused across tests
Parallel-safe — threads never share a driver
driver.remove() required to prevent leaks
Saves startup time in parallel runs
Best for: 100–2000 tests, multi-thread CI
Selenium Grid / Cloud
Tests distributed across many machines
Browser/OS matrix coverage
Built-in video and screenshot capture
Cloud providers: BrowserStack, Sauce Labs
Best for: 2000+ tests, cross-browser suites
Driver lifecycle decisions
Per-method vs per-class. Creating the driver in @BeforeMethod gives full isolation at the cost of startup time per test. Creating it in @BeforeClass shares one driver across all tests in the class — faster, but any test that leaves the browser in an unexpected state affects the next test. @BeforeMethod is the safer default; @BeforeClass is acceptable only for read-only test classes where state mutation is impossible.
Headless in CI. Browser UI rendering requires a display server. CI agents typically have none. Run headless in CI (--headless=new for Chrome, --headless for Firefox) and headed locally:
private static ChromeOptions buildChromeOptions() {
ChromeOptions opts = new ChromeOptions();
opts.addArguments("--no-sandbox", "--disable-dev-shm-usage");
if (Boolean.parseBoolean(System.getenv().getOrDefault("CI", "false"))) {
opts.addArguments("--headless=new", "--window-size=1920,1080");
}
return opts;
}The explicit --window-size in headless mode is non-optional. Without it, Chrome defaults to 800×600 — elements positioned for 1920-wide viewports are either off-screen or in a different layout, causing locators to miss.
Driver warmup. For test classes that share a driver (@BeforeClass), a warmup navigation before the first test ensures the browser session is fully initialised:
@BeforeClass
public void warmUp() {
DriverManager.getDriver().get(Config.baseUrl());
new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(5))
.until(d -> !d.getCurrentUrl().equals("about:blank"));
}⚠️ Common mistakes
- Calling
driver.quit()in@AfterMethodwithoutalwaysRun = true. When a test fails, TestNG's default behaviour skips@AfterMethodif you don't setalwaysRun. The browser is orphaned. In a 200-test parallel suite, this fills the CI agent with zombie processes within minutes. - Passing
driveras a constructor argument to page objects before it's fully initialised. Ifnew LoginPage(driver)is called in a@BeforeClassthat runs before@BeforeMethodcreates the driver,driverisnulland the page object stores a null reference. Always construct page objects after the driver is guaranteed to exist. - Increasing thread count without checking memory. Four threads on a machine with 4 GB RAM, where each Chrome process needs 400 MB, leaves 2.4 GB for the JVM, the OS, and other processes. The suite becomes slower as the OS starts swapping, not faster. Profile memory before scaling thread count.
🎯 Practice task
Implement and validate parallel driver management — 35 minutes.
- Baseline timing. Run your test suite sequentially and time it. Note the average per-test time.
- Implement
DriverManagerwithThreadLocal. Wire it into@BeforeMethodand@AfterMethod(alwaysRun = true). Run the suite sequentially first — confirm all tests pass. - Enable parallel execution. Set
parallel="methods" thread-count="3"in TestNG XML. Run the suite. Confirm all tests pass. If any fail withNoSuchSessionException, check for staticdriverfields in test classes — those must move toThreadLocal. - Time the parallel run. Compare the wall-clock time against the sequential baseline. Expect roughly 2.5–3× speedup for 3 threads on I/O-bound tests. Less than 2× suggests a bottleneck (shared database, slow test data creation).
- Verify teardown. After the parallel suite completes, check for orphan browser processes (
ps aux | grep chromeon macOS/Linux, Task Manager on Windows). Zero orphans meansdriver.remove()andalwaysRun = trueare working correctly.
Next lesson: screenshot and video capture on failure — how to give every CI failure a visual record without any changes to individual test methods.