Why Your Selenium Tests Are Flaky and How to Fix Them (2026 Guide)
Comprehensive guide to fixing flaky Selenium tests. Learn about implicit vs explicit waits, handling stale element exceptions, dynamic content strategies, page load configurations, and Selenium Grid reliability patterns.
selenium flaky testsselenium test reliabilityfix selenium flaky testsselenium wait strategiesselenium stale element exceptionselenium explicit waitselenium implicit waitselenium grid flakyselenium dynamic contentselenium page load strategy
Why Your Selenium Tests Are Flaky and How to Fix Them (2026 Guide)
Selenium remains the most widely used browser automation framework in the world. Despite newer alternatives like Playwright and Cypress, Selenium's language flexibility, mature ecosystem, and broad browser support keep it firmly at the center of most enterprise test automation strategies.
But Selenium has a reputation problem: flaky tests. Ask any test automation engineer about their biggest pain point with Selenium, and "flakiness" will be near the top of the list. The framework's design -- which gives you maximum control but minimal guardrails -- means that writing reliable Selenium tests requires deep understanding of how browsers work, how web applications render, and how to properly synchronize your test code with asynchronous browser operations.
This guide covers the most common causes of flaky Selenium tests and provides battle-tested solutions for each one. Whether you are working with Selenium in Java, Python, C#, or JavaScript, the patterns and principles apply universally.
The Fundamental Problem: Synchronization
Nearly every flaky Selenium test can be traced back to a synchronization problem. Your test code executes on one machine (or process), while the browser executes on another. These two processes run asynchronously. When your test tells the browser to click a button, the test code does not pause and wait for the browser to finish processing the click, render the resulting changes, and settle into a stable state. It just fires the command and moves on.
This fundamental asynchrony is the root of most Selenium flakiness. Your test asserts something before the browser has finished doing what you asked it to do.
Understanding this principle is critical because it shifts your mindset from "Selenium is buggy" to "I need to properly synchronize my test with the browser." The framework is not buggy -- it just does not make synchronization easy or automatic.
Cause 1: Implicit Waits vs. Explicit Waits
The single most common source of Selenium flakiness is improper use of waits. Selenium provides two types of waits, and using them incorrectly -- or mixing them -- is a recipe for flaky tests.
Implicit Waits
Implicit waits tell Selenium to poll the DOM for a specified amount of time when trying to find an element that is not immediately available.
# Setting an implicit wait
driver.implicitly_wait(10) # seconds
Now findElement will wait up to 10 seconds before throwing NoSuchElementException
element = driver.find_element(By.ID, "submit-button")
Problems with implicit waits:
They apply globally. Once set, implicit waits affect every find_element call for the life of the driver session. This makes it hard to have different wait times for different operations.
They only wait for element presence. Implicit waits check whether the element exists in the DOM. They do not wait for the element to be visible, clickable, or enabled. An element can be present in the DOM but hidden, disabled, or covered by another element.
They interact poorly with explicit waits. When you combine implicit and explicit waits, the behavior becomes unpredictable. The total wait time may be the sum of both waits, or it may be just one of them, depending on the driver implementation.
They slow down failure detection. If an element genuinely does not exist (e.g., a test bug or a missing feature), the implicit wait forces Selenium to wait the full timeout before reporting the failure. This makes tests unnecessarily slow when they fail.
Explicit Waits
Explicit waits tell Selenium to wait for a specific condition to be true before proceeding.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
The StaleElementReferenceException is one of the most confusing and frustrating exceptions in Selenium. It occurs when you hold a reference to a DOM element that has been removed from the page or re-rendered.
Why It Happens
Modern web applications constantly re-render parts of the DOM. When a React component re-renders, the old DOM elements are replaced with new ones. Your Selenium element reference still points to the old element, which no longer exists.
# FLAKY: The element reference may become stale between find and click
if stable_count >= 3: # DOM unchanged for 3 consecutive checks
return
else:
stable_count = 0
previous_source = current_source
time.sleep(check_interval)
raise TimeoutError("DOM did not stabilize within timeout")
Cause 3: Dynamic Content and AJAX
Modern web applications load content dynamically through AJAX calls. Tests that do not account for asynchronous content loading are guaranteed to be flaky.
Selenium's page load strategy determines when driver.get() and driver.navigate() consider the page to be loaded. The wrong strategy can cause tests to start interacting with the page before it is ready.
The Three Strategies
from selenium.webdriver.chrome.options import Options
options = Options()
NORMAL (default): Waits for the 'load' event
All resources (images, stylesheets, etc.) are fully loaded
options.page_load_strategy = 'normal'
EAGER: Waits for the 'DOMContentLoaded' event
HTML is parsed and DOM is ready, but resources may still be loading
options.page_load_strategy = 'eager'
NONE: Does not wait at all
Returns immediately after the navigation is initiated
options.page_load_strategy = 'none'
Which Strategy to Use
For most test suites, normal (the default) is the safest choice. It ensures all resources are loaded before your test starts interacting with the page.
Use eager when you are testing a single-page application that does not need all resources to be loaded before the test can interact with it. This speeds up tests but requires more explicit waits.
Use none only when you have full control over your waits and need maximum speed. This is an advanced option that can cause significant flakiness if not handled carefully.
SPA Page Load Pattern
For single-page applications, traditional page load strategies are less relevant because navigation happens within the same page. Use content-based waits instead.
Note: JavaScript click should be a last resort because it bypasses Selenium's built-in checks. If you need JavaScript click frequently, it usually indicates a deeper issue with your test or application.
Element Not Interactable
ElementNotInteractableException: element not interactable
This occurs when an element is present and visible but cannot receive input -- typically because it is disabled, has zero dimensions, or is hidden by CSS.
element = wait.until(EC.element_to_be_clickable((By.ID, "email-input")))
element.clear()
element.send_keys("test@example.com")
Cause 6: Frame and Window Handling
Switching between frames and windows is a frequent source of flakiness because the target frame or window may not be ready when you try to switch to it.
Once you have applied these fixes, you need to measure whether your test suite is actually becoming more reliable. This requires tracking test results over time and computing flakiness metrics.
JUnit XML Output
Configure your test framework to produce JUnit XML reports, which most CI systems and analysis tools can consume.
pytest:
pytest --junitxml=selenium-results.xml
JUnit 5 (Maven):
org.apache.maven.plugins
maven-surefire-plugin
xml
TestNG:
Using DeFlaky for Selenium Test Analysis
DeFlaky can analyze your Selenium test results to identify flaky tests and track reliability trends.
The DeFlaky dashboard provides a per-test view of reliability over time, making it easy to see whether your fixes are holding and to identify new sources of flakiness as they emerge.
Migration Considerations: Selenium to Playwright
For teams considering whether to migrate from Selenium to Playwright to reduce flakiness, here is a honest assessment.
What Playwright Does Better
Auto-waiting: Playwright waits for elements to be actionable by default. This eliminates the need for most explicit waits.
Browser contexts: Lightweight isolation without creating new browser instances. Faster and more reliable test isolation.
Network interception: Built-in request mocking and interception. No need for external proxy tools.
Tracing: Built-in trace capture for debugging failures.
What Selenium Still Does Better
Language support: Selenium supports more languages (Java, Python, C#, Ruby, JavaScript, Kotlin). Playwright supports JavaScript/TypeScript, Python, Java, and C#.
Ecosystem maturity: Selenium has 20 years of community-built tools, frameworks, and integrations.
Browser support: Selenium supports more browsers and browser versions through WebDriver protocol.
Enterprise adoption: Most large enterprises have invested heavily in Selenium. Migration cost is significant.
The Pragmatic Approach
You do not have to migrate your entire suite. Consider using Playwright for new tests while maintaining existing Selenium tests with improved reliability patterns. Tools like DeFlaky can track flakiness across both frameworks, giving you data to inform your migration decisions.
A Selenium Reliability Checklist
Use this checklist when writing or reviewing Selenium tests.
Waits
[ ] No time.sleep() or Thread.sleep() calls (use explicit waits instead)
[ ] No implicit waits set (use explicit waits exclusively)
[ ] Explicit waits use appropriate expected conditions (not just presence_of_element_located)
[ ] Custom expected conditions for application-specific states
[ ] Timeouts are appropriate (not too short, not excessively long)
Element Interaction
[ ] Re-find elements after any page navigation or re-render
[ ] Handle StaleElementReferenceException with retries
[ ] Scroll elements into view before clicking
[ ] Wait for covering elements (modals, spinners) to disappear
[ ] Use explicit waits before form input
Test Isolation
[ ] Each test creates its own test data
[ ] Cookies and local storage cleared between tests
[ ] No shared mutable state between tests
[ ] Tests pass when run in any order
Environment
[ ] Consistent browser window size set explicitly
[ ] Headless mode for CI with appropriate flags
[ ] Page load timeout configured
[ ] No dependency on local timezone or locale
CI/CD
[ ] JUnit XML reports generated for result tracking
[ ] Screenshots captured on failure
[ ] Browser logs captured on failure
[ ] Retry mechanism for infrastructure-related failures
Conclusion
Selenium test flakiness is not a fundamental limitation of the framework -- it is a consequence of the framework's design philosophy of giving developers full control without guardrails. By understanding the root causes of flakiness and applying the patterns described in this guide, you can build a Selenium test suite that is reliable, maintainable, and trustworthy.
The key principles are straightforward: use explicit waits with appropriate conditions, handle stale element references, isolate test state, and account for asynchronous content loading. Apply these consistently, measure your reliability with tools like DeFlaky, and your Selenium tests will become an asset rather than a liability.
Reliable tests are not about using the "right" framework. They are about understanding how browsers work and synchronizing your test code accordingly. Master that, and your tests will be reliable regardless of which framework you choose.
Stop guessing. DeFlaky your tests.
Detect flaky tests in minutes with a single CLI command.