Intermittent Selenium failures often come from a timing mismatch: the browser has loaded a page, but the application has not finished rendering the part your test needs. The quickest useful fix is to wait for a specific application condition, find elements again after the page replaces them, and check for overlays before clicking. Avoid adding a long sleep or retrying the whole test; those approaches can hide the cause or repeat an action.
Scope: The Java examples use Java 17 and Selenium Java 4.34.0 as an explicit example baseline. This is a guide baseline, not a claim that the examples were executed or that these are the newest versions. Browser and driver versions can affect behavior, so record them when diagnosing CI failures. The guide uses an explicit wait only; it does not configure an implicit wait.
Reproduce the intermittent failure
Start with a local page rather than a remote site. A small fixture makes the timing and DOM changes predictable, and it avoids dependence on an external service. The example page has three controls: a button that appears after a delay, a button whose DOM node is replaced, and a button temporarily covered by an overlay.
Create a folder named selenium-flaky-demo, then save this complete fixture as selenium-flaky-demo/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Selenium timing fixture</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
section { margin: 1.5rem 0; padding: 1rem; border: 1px solid #aaa; }
#cover {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: rgb(0 0 0 / 45%);
}
#cover[hidden] { display: none; }
#cover p { padding: 1rem; background: white; }
</style>
</head>
<body>
<h1>Timing fixture</h1>
<section aria-labelledby="delayed-heading">
<h2 id="delayed-heading">Delayed render</h2>
<div id="delayed-area" aria-live="polite"></div>
</section>
<section aria-labelledby="rerender-heading">
<h2 id="rerender-heading">Node replacement</h2>
<div id="rerender-area">
<button id="refresh-node" type="button">Replace target</button>
<button id="rerender-target" type="button">Old target</button>
<span id="rerender-status" aria-live="polite">Initial node</span>
</div>
</section>
<section aria-labelledby="overlay-heading">
<h2 id="overlay-heading">Temporary overlay</h2>
<button id="overlay-target" type="button">Continue</button>
<span id="overlay-status" aria-live="polite">Not continued</span>
</section>
<div id="cover" role="dialog" aria-label="Temporary notice">
<p>A temporary notice is covering the page.</p>
</div>
<script>
window.setTimeout(() => {
const button = document.createElement("button");
button.id = "delayed-button";
button.type = "button";
button.textContent = "Finish delayed step";
button.addEventListener("click", () => {
document.getElementById("delayed-area").textContent = "Delayed step complete";
});
document.getElementById("delayed-area").replaceChildren(button);
}, 900);
document.getElementById("refresh-node").addEventListener("click", () => {
const oldButton = document.getElementById("rerender-target");
const newButton = oldButton.cloneNode(true);
newButton.textContent = "New target";
newButton.addEventListener("click", () => {
document.getElementById("rerender-status").textContent = "New node clicked";
});
oldButton.replaceWith(newButton);
document.getElementById("rerender-status").textContent = "Node replaced";
});
document.getElementById("overlay-target").addEventListener("click", () => {
document.getElementById("overlay-status").textContent = "Continued";
});
window.setTimeout(() => {
document.getElementById("cover").hidden = true;
}, 1800);
</script>
</body>
</html>
Serve the folder from a terminal. Keep the server running in that terminal while you run the Java example:
cd selenium-flaky-demo
python3 -m http.server 8000 --bind 127.0.0.1
Open http://127.0.0.1:8000/ in a browser. The delayed button should appear, the replacement control should change the target button’s text, and the overlay should disappear. These are expected fixture behaviors from the code; they are not results of a reported browser run.
Page loaded versus app ready
A successful navigation tells you that the browser reached a document readiness point. It does not prove that JavaScript-driven content is ready for the next test step. An application may fetch data, render a component later, replace a node, or display a temporary layer after navigation. Selenium’s waits guidance makes this distinction: navigation readiness and readiness of dynamic page content are separate concerns. See Selenium’s waits documentation.
It helps to separate readiness into several checks:
- Navigation: the browser has opened the target URL.
- DOM presence: an element matching a locator exists in the document.
- Visibility: the element is displayed, rather than merely present in the DOM.
- Interaction readiness: the element can reasonably receive the intended action.
- Business readiness: the application is in the state required by the test, such as a completed search or enabled submit control.
These conditions are not interchangeable. For example, waiting for #delayed-button to exist is useful when the button is created later. But existence alone does not establish that an element is visible or that the application has completed the user-facing step. For an outcome, wait for an outcome: in this fixture, clicking the button should eventually make #delayed-area contain “Delayed step complete”.
A useful test describes the user action and the resulting state. It should not assume that a fixed amount of time is enough. A delay that happens to work on a developer’s laptop may be too short on a busy CI worker and waste time when the application responds quickly.
Choose the right explicit wait
An explicit wait repeatedly checks a condition until the condition succeeds or a timeout is reached. In Selenium Java, WebDriverWait uses a timeout and a polling interval to check a condition. Selenium’s expected-condition support includes common checks such as visibility and clickability. Choose a condition that matches the next operation, and use a locator so Selenium can find the element when the condition is evaluated. See Selenium’s expected conditions reference.

For comparison, this is a fragile sleep-based excerpt, not a complete file:
Thread.sleep(2000);
driver.findElement(By.id("delayed-button")).click();
If the button appears later than the sleep, the lookup fails. If it appears sooner, the test waits longer than needed. A sleep can be appropriate when deliberately controlling a test fixture or investigating a timing issue, but it should not be the normal readiness strategy.
The following is a complete small Maven example. It navigates to the local fixture, waits for the covering layer to disappear, then waits for the delayed button, clicks it, and checks the resulting text. Save it as selenium-flaky-demo/java-demo/pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>selenium-flaky-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<selenium.version>4.34.0</selenium.version>
</properties>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<mainClass>example.TimingWalkthrough</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Save this complete Java source as selenium-flaky-demo/java-demo/src/main/java/example/TimingWalkthrough.java:
package example;
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class TimingWalkthrough {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
try {
driver.get("http://127.0.0.1:8000/");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(8));
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("cover")));
By delayedButton = By.id("delayed-button");
WebElement button = wait.until(
ExpectedConditions.elementToBeClickable(delayedButton));
button.click();
By delayedStatus = By.id("delayed-area");
wait.until(ExpectedConditions.textToBe(
delayedStatus, "Delayed step complete"));
System.out.println("The delayed step reached its expected state.");
} finally {
driver.quit();
}
}
}
With Java 17 and Maven available, run the fixture server in one terminal. In another, enter selenium-flaky-demo/java-demo and run:
mvn compile exec:java
Chrome must be available to Selenium’s driver setup. Driver discovery and browser installation depend on the local environment, so check your project’s browser and driver configuration if Chrome does not start. Do not treat a startup error as a wait problem.
The program prints its status line only after the expected page text is observed. That line is illustrative output based on the code path, not a claim that this example was executed. If the status condition times out, investigate whether the fixture server is running, whether the URL is reachable, whether the button was clicked, and whether the locator still matches the page.
Selenium’s implicit wait defaults to zero. An implicit wait affects element-location calls, while an explicit wait checks a chosen condition. Combining them can create unpredictable wait durations, so keep this walkthrough on explicit waits and do not add driver.manage().timeouts().implicitlyWait(...). Selenium explains the interaction between wait strategies.
| Approach | What it waits for | Typical risk |
|---|---|---|
| Fixed sleep | A set amount of elapsed time | Too short fails; too long wastes time |
| Implicit wait | Element location to succeed, up to a configured timeout | Can complicate timing when mixed with explicit waits |
| Explicit wait | A selected condition, checked until success or timeout | Weak or unrelated conditions can still allow a bad next step |
Stale elements after a re-render
A stale element reference means the element handle held by the test no longer points to a current node in the page. This can happen when a framework replaces a node during a render. The old button may look much like the new one, but a stored reference to the old node is not automatically updated. Selenium lists stale element references as a distinct WebDriver error. See Selenium’s WebDriver error troubleshooting guide.

The fixture makes this easy to reproduce. The “Replace target” button replaces #rerender-target with a cloned node. A test that saves the target first and then replaces it is holding a reference to the old node. Trying to interact with that reference can raise StaleElementReferenceException.
This excerpt shows the risky order:
WebElement oldTarget = driver.findElement(By.id("rerender-target"));
driver.findElement(By.id("refresh-node")).click();
oldTarget.click();
The safer pattern is to perform the change, wait for the condition that indicates the replacement, then locate the target again. A locator is a description of how to find an element; it is not a long-lived element handle. For example:
By target = By.id("rerender-target");
driver.findElement(By.id("refresh-node")).click();
wait.until(ExpectedConditions.textToBe(
By.id("rerender-status"), "Node replaced"));
WebElement currentTarget = wait.until(
ExpectedConditions.elementToBeClickable(target));
currentTarget.click();
wait.until(ExpectedConditions.textToBe(
By.id("rerender-status"), "New node clicked"));
This is a small excerpt that can be added inside the Java program after creating wait. Its expected final state is “New node clicked”. The important detail is not simply “retry the click”; it is to re-find the element after the page has changed, then verify the user-visible result.
When possible, wait for a meaningful state change rather than a guessed rendering delay. Here the fixture updates #rerender-status to “Node replaced”, which gives the test a useful synchronization point. In a real application, choose a stable signal such as a loading indicator disappearing, a result count changing, or the newly rendered component becoming visible.
Do not respond to every stale reference by retrying the same element handle. It remains stale. Also avoid wrapping the whole test in a retry loop: if an earlier step submitted a form or created a record, rerunning it may produce a second side effect. A bounded retry can be appropriate for a carefully identified, safe operation, but it must be designed around the operation’s consequences and must reacquire current elements.
Overlays and intercepted clicks
An ElementClickInterceptedException means another element prevented the requested click from reaching its target. A modal, loading layer, sticky banner, or animation can cover the target. This is not the same failure as a stale element: one is an interaction obstruction, the other is an outdated element reference. Selenium documents intercepted interactions as a separate error category. Use the error type to guide diagnosis.
In the fixture, the overlay covers the page for 1.8 seconds. Clicking #overlay-target before it disappears can be intercepted. A presence check for the target will not solve this: the button can exist underneath the overlay.
Wait for the specific overlay to become invisible, then locate and click the target. Add this excerpt inside the Java program after creating wait:
By overlay = By.id("cover");
By continueButton = By.id("overlay-target");
wait.until(ExpectedConditions.invisibilityOfElementLocated(overlay));
WebElement currentButton = wait.until(
ExpectedConditions.elementToBeClickable(continueButton));
currentButton.click();
wait.until(ExpectedConditions.textToBe(
By.id("overlay-status"), "Continued"));
The check names the known obstruction instead of waiting for an arbitrary delay. The clickability condition is useful, but it is not a promise that no transient overlay can appear between the condition check and the click. If the click is still intercepted, capture evidence and investigate whether another layer appeared, the page moved, or the target locator selected the wrong element. Selenium’s expected conditions help express conditions; they cannot remove every race in a changing application. Review the available expected conditions.
Do not replace a failed user interaction with JavaScript that directly calls the element’s click handler. That can hide the fact that a real user would be blocked by an overlay or other layout problem. First determine whether the overlay should be dismissed, whether the page is still loading, and whether the test is acting at the right time. For JavaScript alerts or pop-ups, use the appropriate browser interaction rather than treating them as ordinary page overlays; see Java Selenium alerts and pop-ups.
Clicks on non-idempotent actions need extra care. An operation is non-idempotent when repeating it can change the result again—for example, placing an order, creating a payment, or submitting a one-time form. A retry intended to recover from a flaky click might submit twice if the first click reached the application but the test failed to observe the response. Prefer waiting for a clear state and checking the result. If the action must be retried, the application and test need a safe way to identify and handle duplicate submissions.
Make failures diagnosable in CI
A timeout tells you a condition did not become true before the deadline. It does not tell you why. Useful failure evidence narrows the possibilities: Was the wrong page open? Did the element never appear? Was it present but hidden? Did an overlay remain? Did the browser fail before the test reached the page?
For a failed interaction, collect a screenshot and the context needed to diagnose it. Review captured information before sharing it. This complete helper can be saved as selenium-flaky-demo/java-demo/src/main/java/example/FailureEvidence.java and called from a test or main-class catch block. It writes a screenshot, current URL, page title, and page source to a chosen directory. Page source can contain private data, so review and redact it before storing or uploading it in CI.
package example;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
public final class FailureEvidence {
private FailureEvidence() {
}
public static void capture(WebDriver driver, Path directory) throws IOException {
Files.createDirectories(directory);
Path screenshot = directory.resolve("failure.png");
byte[] image = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
Files.write(screenshot, image);
String context = "URL: " + safe(driver.getCurrentUrl())
+ System.lineSeparator()
+ "Title: " + safe(driver.getTitle())
+ System.lineSeparator();
Files.writeString(
directory.resolve("context.txt"),
context,
StandardCharsets.UTF_8);
Files.writeString(
directory.resolve("page.html"),
safe(driver.getPageSource()),
StandardCharsets.UTF_8);
}
private static String safe(String value) {
return value == null ? "(unavailable)" : value;
}
}
This helper does not automatically sanitize the page source. Treat it as sensitive until your team has reviewed what the application renders. Avoid capturing passwords, tokens, payment details, personal information, or other user data in artifacts. Restrict artifact access according to your organization’s normal policies and keep only what helps diagnose the failure.
Also record the environment alongside the failure: Java version, Selenium version, browser name and version, driver version, operating system, headless or headed mode, and the test’s relevant timeout. These are investigation details, not proof that a particular configuration caused the problem. Do not describe a configuration as tested unless it was actually run and recorded.
Compare a local run and CI only when the evidence makes that comparison useful. A different viewport, browser build, CPU load, or network path can expose timing assumptions. The remedy is still to synchronize with the application’s state and inspect the failing page—not to enlarge every timeout without checking what is slow.
Organise maintainable tests
When several tests need the same readiness condition, put the locator and wait logic in a small helper or page object. A page object is a class that groups the locators and interactions for a page or component. Keep it focused: it should express useful actions and readiness signals, not hide arbitrary sleeps or silently retry every failure.
For example, this excerpt is a small helper method for a page object, not a complete Java file:
private final WebDriverWait wait;
public WebElement delayedActionButton() {
return wait.until(ExpectedConditions.elementToBeClickable(
By.id("delayed-button")));
}
Use such a helper where the test needs the delayed action, then assert the resulting state after the action. If the page replaces nodes, have the helper locate the element when it is needed rather than storing a WebElement for the lifetime of the test. Be clear about the distinction between a method that waits for a control and a method that performs a business action.
Timeouts should reflect how long the application is reasonably allowed to reach the expected state in the environment, not an attempt to guarantee success. When a timeout is exceeded, make the failure message and captured context useful. If many unrelated waits require very large values, investigate whether the application or test setup is unhealthy before increasing all of them.
Accessibility is another useful part of reliable UI coverage. Stable accessible names and semantic controls help tests identify what a user can interact with, while also supporting people who use assistive technology. A robust locator still needs an appropriate readiness condition, and accessibility checks do not replace functional assertions. For a separate introduction, see accessibility testing.
Choose the next diagnostic step

| Failure | What it usually points to | Next useful check |
|---|---|---|
NoSuchElementException or wait timeout |
The locator did not match, the page was not ready, or the expected state never occurred | Check the URL, locator, page source, and whether you waited for the right condition |
StaleElementReferenceException |
The page replaced the node represented by a saved element handle | Wait for the relevant update, then locate the element again by its locator |
ElementClickInterceptedException |
Another element covered or obstructed the target at click time | Capture a screenshot, identify the covering layer, and wait for its intended resolution |
| Browser or driver startup failure | The failure happened before application readiness was relevant | Record browser, driver, Selenium, Java, and environment details |
These are diagnostic starting points, not guarantees about every cause. Read the exception message and inspect the browser state. Selenium’s error guide provides more detail on WebDriver failures: troubleshooting WebDriver errors.
Verification checklist
Before treating a flaky-test fix as complete, work through these steps in the environment where the issue matters:
- Confirm Java 17, Selenium Java 4.34.0, the browser, and the driver versions are recorded for the run.
- Start the local fixture with
python3 -m http.server 8000 --bind 127.0.0.1and confirm the page loads athttp://127.0.0.1:8000/. - Use an explicit wait for a condition that matches the next step; do not rely on a fixed sleep or add an implicit wait.
- After a DOM replacement, reacquire the element from its locator and assert the resulting page state.
- Before clicking a covered control, wait for the specific overlay to become invisible and still inspect any intercepted click.
- Check that retries cannot repeat a purchase, submission, or other non-idempotent action.
- In CI, inspect screenshots and relevant page context while removing sensitive user data from artifacts.
- If you change the fixture’s delay or run headless, record the actual results for those runs before making claims about stability.
The useful outcome is not simply a test that passes once. It is a test whose wait describes the application state it needs, whose element references remain current, and whose failure evidence helps explain what happened when the state was not reached.
Dont SPAM