Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallUse a separate WebDriver for each parallel test thread, store that reference in a ThreadLocal<WebDriver>, and close the session and remove the thread-local value during teardown. ThreadLocal keeps references separate; it does not make one shared driver safe for concurrent use. The examples below use explicit startup to avoid accidentally creating a browser during cleanup.
What ThreadLocal does in a Selenium test
Java’s ThreadLocal<T> gives each thread that accesses the same ThreadLocal object its own independently initialized value. For Selenium, that means two test worker threads can retrieve different WebDriver references from the same driver store. Java’s API also supports lazy initialization with ThreadLocal.withInitial(Supplier); a call to get() creates the value for that thread if it has none. See the Java SE 26 ThreadLocal API.
This is an ownership and access pattern, not a way to share one browser safely. Create a driver on the test’s worker thread, use it only on that thread, and dispose of it there. A test runner must keep setup, test execution, and teardown on the same thread for this pattern to work as intended.
Implement a per-thread driver store
An explicit-start design makes the lifecycle visible and lets teardown check for an existing value without triggering a lazy initializer.
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public final class DriverStore {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
private DriverStore() {}
public static void start() {
DRIVER.set(new ChromeDriver());
}
public static WebDriver getDriver() {
WebDriver driver = DRIVER.get();
if (driver == null) {
throw new IllegalStateException("WebDriver has not been started on this thread");
}
return driver;
}
public static void quitDriver() {
WebDriver driver = DRIVER.get();
try {
if (driver != null) {
driver.quit();
}
} finally {
DRIVER.remove();
}
}
}
This class assumes the project already has Selenium’s Java binding and a usable Chrome setup. The Selenium documentation describes WebDriver as able to drive browsers locally or through Selenium Server; it also points to Selenium Manager for default driver/browser management in bindings. Use the versions and configuration pinned by your project rather than assuming a particular setup. See Selenium WebDriver documentation.
Wire startup and cleanup into the test lifecycle
Call start() in per-test setup, obtain the driver through getDriver() in the test, and put quitDriver() in a teardown or always-run cleanup hook. The framework-specific annotation varies by JUnit or TestNG version, so use the hook that your runner guarantees will run after test failures and exceptions. Selenium’s organization page lists both JUnit and TestNG for Java, but labels its content incomplete; it is not a substitute for the runner’s own lifecycle documentation.
// Pseudocode for a test framework's lifecycle hooks:
@BeforeEach
void startBrowser() {
DriverStore.start();
}
@Test
void checkPage() {
DriverStore.getDriver().get("https://example.com");
}
@AfterEach
void stopBrowser() {
DriverStore.quitDriver();
}
The annotations above illustrate lifecycle placement, not a complete framework-specific class; imports and exact hook names depend on the selected runner and version.
Rank #2
Alternative: lazy initialization
You can initialize on first access with ThreadLocal.withInitial:
private static final ThreadLocal<WebDriver> DRIVER =
ThreadLocal.withInitial(ChromeDriver::new);
public static WebDriver getDriver() {
return DRIVER.get();
}
public static void quitDriver() {
WebDriver driver = DRIVER.get();
try {
if (driver != null) {
driver.quit();
}
} finally {
DRIVER.remove();
}
}
Be careful with this form: get() initializes a value when none exists. If teardown calls it for a test that never reached browser startup, cleanup can create a new browser just to close it. Prefer explicit startup or a design that can inspect whether a value exists without invoking the initializer.
Why quit and remove are both necessary
driver.quit() ends the browser session. DRIVER.remove() clears the current thread’s stored reference. Closing the remote or local session does not itself remove the Java thread-local value.
This matters especially with thread pools: worker threads can outlive individual tests and be reused for later tasks. Oracle’s guidance warns that a thread-local value can remain for the thread’s lifetime or until removal; without cleanup, state from one task may remain available to a later task. Put removal in a finally block so it still runs if quit() throws. See Oracle’s ThreadLocal variables guidance.
Keep driver use on the creating thread
Do not pass the driver reference to another thread, including a helper task running on a different executor thread. The thread-local store selects a value by the thread calling get(); it does not transfer ownership or make WebDriver operations thread-safe.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesSelenium’s Java ThreadGuard can wrap a driver and detect calls made from a thread other than the one that created it. Selenium explicitly cautions: “This does not replace the need for using ThreadLocal to manage drivers when running parallel.” Treat ThreadGuard as a diagnostic check, not a substitute for per-thread lifecycle management. See Selenium ThreadGuard documentation.
Rank #4
Local drivers and Selenium Grid solve different problems
| Choice | Where the browser runs | Main purpose | ThreadLocal implication |
|---|---|---|---|
| Local WebDriver | On the test machine | Local development or a suite running on one machine | Keep a separate driver reference for each concurrently executing test thread. |
| RemoteWebDriver through Grid | On a remote Grid node | Parallel execution across machines, browser versions, and platforms | Each parallel test still needs its own session and driver reference on its executing thread. |
Selenium Grid routes commands to remote browser instances and is designed to support parallel, cross-browser, and cross-platform execution. It changes where browser sessions run; it does not eliminate the need to assign and clean up one driver per concurrent test. See Selenium Grid documentation.
Common problems and fixes
- Two tests control the same browser: a global static
WebDriverwas likely shared directly. Store a distinct driver per worker thread and do not hand references across threads. - “WebDriver has not been started on this thread”: the current thread did not execute setup, or the test is running on a different thread than setup. Check the runner’s scheduling and keep lifecycle operations on one worker.
- A browser starts during teardown unexpectedly: cleanup probably called
get()on aThreadLocal.withInitialvariable. Use explicit initialization or a non-initializing cleanup path. - Later tests inherit stale driver state: teardown may be missing, may not run on failure, or may run on a different worker. Ensure
quit()andremove()are both executed in the test’s cleanup path. - ThreadGuard reports cross-thread access: some code invoked the driver from a thread other than its creator. Move the browser operation to the owning test thread rather than disabling the guard.
- Parallel tests still fail despite ThreadLocal: ThreadLocal isolates only the driver reference. It does not synchronize shared test data, static application state, files, accounts, or other resources; isolate or coordinate those separately.
Or skip the browser setup
If your goal is to capture a website image or PDF rather than run an interactive Selenium test, ScreenshotNeo provides a one-request screenshot API. It is not a replacement for Selenium test automation; it can avoid browser setup for capture tasks.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for parameters and response details. Before capture, it accepts cookie/consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets; each of those steps can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000.
Sign up for 1,000 free screenshots a month, with no card required.
Best Value
Further reading
- Java SE 26 ThreadLocal API for per-thread values, initializers, and removal behavior.
- Selenium ThreadGuard for cross-thread call detection.
- Selenium Grid for remote browser execution and distributed scaling.
Frequently Asked Questions
Does ThreadLocal create a new browser for every test?
Only if your test setup creates one for each test execution. ThreadLocal provides per-thread storage; it does not define test boundaries or automatically close browser sessions.
Can setup and teardown run on different threads?
This pattern expects driver creation, use, and cleanup on the same worker thread. Confirm the lifecycle and scheduling guarantees of your specific test runner.
Does ThreadGuard replace ThreadLocal?
No. ThreadGuard detects cross-thread driver calls; Selenium says it does not replace ThreadLocal for parallel driver management.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




