Fix: Selenium Not Working — WebDriver Errors, Element Not Found, and Timeout Issues
Part of: Python Errors
Quick Answer
How to fix Selenium errors — WebDriverException session not created, NoSuchElementException element not found, StaleElementReferenceException, TimeoutException waiting for element, headless Chrome crashes, and driver version mismatch.
The Error
You try to start a browser and Selenium refuses:
selenium.common.exceptions.WebDriverException: Message: session not created:
This version of ChromeDriver only supports Chrome version 120
Current browser version is 124.0.6367.91Or the element is clearly visible on the page but Selenium can’t find it:
selenium.common.exceptions.NoSuchElementException: Message: no such element:
Unable to locate element: {"method":"css selector","selector":"#login-button"}Or you find an element, interact with it, and get a stale reference error:
selenium.common.exceptions.StaleElementReferenceException: Message: stale element
reference: stale element not found in the current active documentOr the test times out waiting for a page to load:
selenium.common.exceptions.TimeoutException: Message: Timed out waiting for page load.Selenium controls a real browser — Chrome, Firefox, Edge — through a WebDriver binary that must match the browser version exactly. The browser renders pages asynchronously, so elements may not exist yet when Selenium looks for them. This guide covers every common failure mode.
Why This Happens
Selenium issues fall into three categories: driver setup (WebDriver version mismatch, missing binary, wrong PATH), timing (elements not yet rendered, stale references after page navigation), and element location (wrong selector, element inside an iframe, element hidden or overlapped).
The timing category causes the most confusion. A browser renders HTML progressively — JavaScript loads asynchronously, AJAX calls fire after page load, and single-page apps rebuild the DOM on navigation. If Selenium tries to find an element before the JavaScript creates it, the element doesn’t exist yet.
Diagnostic Timeline
A real “Selenium can’t find this button” session usually plays out like this. Use the timeline to skip the first three wrong guesses.
Minute 0 — first guess: raise the wait. You assume the page is slow, so you bump WebDriverWait from 10 to 30 seconds. The error is identical: NoSuchElementException. Longer waits only help when the element eventually appears in the same document — not when it lives somewhere Selenium isn’t looking.
Minute 3 — second guess: tweak the selector. You rewrite the CSS to be more specific, then switch to XPath. Still nothing. You paste the selector into DevTools console (document.querySelector(...)) — and it returns the element. So the selector is correct in the browser; Selenium just can’t see it.
Minute 8 — driver/browser version mismatch. Chrome auto-updated overnight; your pinned chromedriver is one major version behind. Symptom: session not created, or stale automation flags. Upgrade to Selenium 4.6+ so Selenium Manager picks the matching driver, or pin both browser and driver in your CI image so they update together.
Minute 15 — iframe context. A login form, payment widget, or embedded video is inside an <iframe>. Selenium’s find_element only searches the current frame. You confirm in DevTools: the element’s ancestors include <iframe> before reaching <html>. Call driver.switch_to.frame(iframe_element) first, then locate. switch_to.default_content() returns to the top document.
Minute 25 — shadow DOM piercing. Web components (chat widgets, design systems like Lit/Polymer/Stencil) attach a shadow root that ordinary selectors cannot pierce. Symptom: the element shows in DevTools but document.querySelector returns null. Reach in via JavaScript: driver.execute_script("return document.querySelector('host').shadowRoot.querySelector('#button')"). Each closed shadow root needs its own hop.
Minute 40 — the real culprit. The element exists only after a hover, scroll, or after a service worker resolves a token. Use WebDriverWait with a custom lambda that performs the trigger before asserting presence. Once you know it’s a state precondition rather than a timing problem, every retry becomes deterministic.
Fix 1: WebDriver Version Mismatch — Use Selenium Manager
WebDriverException: session not created: This version of ChromeDriver
only supports Chrome version 120Selenium 4.6+ includes Selenium Manager — it automatically downloads the correct driver for your installed browser. If you’re on Selenium 4.6+, you don’t need to manage drivers manually:
from selenium import webdriver
# Selenium 4.6+ — driver management is automatic
driver = webdriver.Chrome() # Downloads matching ChromeDriver automatically
driver.get("https://example.com")
driver.quit()Check your Selenium version:
pip show selenium
# Version: 4.20.0 — Selenium Manager includedIf automatic management fails (corporate proxy, restricted network), install webdriver-manager:
pip install webdriver-managerfrom selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
# Manually manage driver version
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)For Firefox:
from selenium import webdriver
driver = webdriver.Firefox() # Selenium Manager handles geckodriverFor Edge:
from selenium import webdriver
driver = webdriver.Edge() # Selenium Manager handles msedgedriverCommon causes of driver errors even with Selenium Manager:
- Chrome installed via Snap on Ubuntu — Selenium Manager can’t find the Snap-installed Chrome. Install Chrome via
.debinstead - Corporate proxy — Selenium Manager can’t download the driver. Set
SE_MANAGER_PROXYenv var - Outdated Selenium — versions before 4.6 don’t have Selenium Manager. Upgrade:
pip install --upgrade selenium
Fix 2: NoSuchElementException — Element Not Found
NoSuchElementException: Unable to locate element: {"method":"css selector","selector":"#submit-btn"}The element either doesn’t exist yet (page still loading), is inside an iframe, or the selector is wrong.
Step 1: Use explicit waits instead of find_element directly:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example.com")
# WRONG — element may not exist yet
button = driver.find_element(By.ID, "submit-btn") # NoSuchElementException
# CORRECT — wait up to 10 seconds for the element to appear
wait = WebDriverWait(driver, 10)
button = wait.until(EC.element_to_be_clickable((By.ID, "submit-btn")))
button.click()Never use time.sleep() for waiting — it’s fragile and slow:
import time
# WRONG — waits 5 seconds even if element appears in 0.5s
time.sleep(5)
button = driver.find_element(By.ID, "submit-btn")
# CORRECT — waits up to 10s, returns as soon as element appears
button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "submit-btn"))
)Common expected conditions:
from selenium.webdriver.support import expected_conditions as EC
# Wait for element to be present in DOM (may be hidden)
EC.presence_of_element_located((By.CSS_SELECTOR, ".modal"))
# Wait for element to be visible (present AND displayed)
EC.visibility_of_element_located((By.ID, "results"))
# Wait for element to be clickable (visible AND enabled)
EC.element_to_be_clickable((By.XPATH, "//button[text()='Submit']"))
# Wait for element to disappear (loading spinner gone)
EC.invisibility_of_element_located((By.CLASS_NAME, "spinner"))
# Wait for text to appear in element
EC.text_to_be_present_in_element((By.ID, "status"), "Complete")
# Wait for URL to change
EC.url_contains("dashboard")Step 2: Check if the element is inside an iframe:
# Elements inside iframes are invisible to the main page's DOM
# Switch to the iframe first, then find the element
# By iframe element
iframe = driver.find_element(By.TAG_NAME, "iframe")
driver.switch_to.frame(iframe)
button = driver.find_element(By.ID, "submit-btn") # Now visible
button.click()
# Switch back to main page
driver.switch_to.default_content()Step 3: Verify your selector in browser DevTools. Open F12 → Console → type:
document.querySelector("#submit-btn") // CSS selector
document.querySelectorAll(".item") // All matchesIf the selector returns null in DevTools, the selector is wrong — not a Selenium issue.
Fix 3: StaleElementReferenceException — Element Changed
StaleElementReferenceException: stale element not found in the current active documentYou found an element, the page refreshed or the DOM changed (AJAX update, SPA navigation), and now the reference points to a deleted DOM node.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# WRONG — element goes stale after page navigation
items = driver.find_elements(By.CSS_SELECTOR, ".product")
for item in items:
item.click() # StaleElementReferenceException after first click
driver.back() # Page reloads, all item references are stale
# CORRECT — re-find elements after each navigation
product_count = len(driver.find_elements(By.CSS_SELECTOR, ".product"))
for i in range(product_count):
items = driver.find_elements(By.CSS_SELECTOR, ".product") # Fresh reference
items[i].click()
# Process product page...
driver.back()
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".product"))
)For SPAs that update the DOM without full page loads:
from selenium.common.exceptions import StaleElementReferenceException
def click_with_retry(driver, locator, retries=3):
for attempt in range(retries):
try:
element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable(locator)
)
element.click()
return
except StaleElementReferenceException:
if attempt == retries - 1:
raiseFix 4: Headless Chrome — Running Without a Display
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--headless=new") # New headless mode (Chrome 112+)
options.add_argument("--no-sandbox") # Required in Docker/CI
options.add_argument("--disable-dev-shm-usage") # Prevent /dev/shm crashes in Docker
driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
print(driver.title)
# Take screenshot for debugging
driver.save_screenshot("debug.png")
driver.quit()--headless=new vs --headless:
The old --headless flag used a separate rendering path that behaved differently from regular Chrome. --headless=new (Chrome 112+) uses the same rendering engine — fewer compatibility issues and identical behavior to headed mode.
Common headless issues:
options = Options()
options.add_argument("--headless=new")
# Set window size — headless defaults to 800x600 which can hide elements
options.add_argument("--window-size=1920,1080")
# Disable GPU (required on some systems)
options.add_argument("--disable-gpu")
# User-agent — some sites block headless Chrome's default user agent
options.add_argument(
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)For general Python package installation failures when setting up Selenium in CI/CD or Docker, see pip could not build wheels.
Docker setup:
FROM python:3.12-slim
RUN apt-get update && apt-get install -y \
chromium \
chromium-driver \
&& rm -rf /var/lib/apt/lists/*
RUN pip install selenium
# Set Chrome binary location for Selenium
ENV CHROME_BIN=/usr/bin/chromiumfrom selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.binary_location = "/usr/bin/chromium"
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=options)Fix 5: Clicking Elements — Intercepted, Hidden, or Overlapped
ElementClickInterceptedException: element click intercepted:
Element <button> is not clickable at point (x, y).
Other element would receive the click: <div class="overlay">Another element (modal, cookie banner, overlay) is covering the button.
Fix 1: Close the overlay first:
# Dismiss cookie consent
try:
cookie_btn = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable((By.ID, "accept-cookies"))
)
cookie_btn.click()
except:
pass # No cookie banner presentFix 2: Scroll the element into view:
from selenium.webdriver.common.action_chains import ActionChains
element = driver.find_element(By.ID, "target-button")
# Scroll into view
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
# Then click
element.click()Fix 3: Click via JavaScript (bypasses overlay interception):
element = driver.find_element(By.ID, "target-button")
driver.execute_script("arguments[0].click();", element)Pro Tip: JavaScript clicks bypass Selenium’s visibility and interception checks entirely. Use them only when the element is genuinely clickable but Selenium’s click detection is wrong (e.g., a sticky header overlapping the element). For testing, prefer native Selenium clicks — they simulate real user behavior more accurately.
Fix 4: Wait for the overlay to disappear:
# Wait for loading overlay to vanish
WebDriverWait(driver, 15).until(
EC.invisibility_of_element_located((By.CLASS_NAME, "loading-overlay"))
)
# Now click
driver.find_element(By.ID, "target-button").click()Fix 6: Handling Dropdowns, Alerts, and New Windows
Select dropdowns:
from selenium.webdriver.support.ui import Select
dropdown = Select(driver.find_element(By.ID, "country"))
dropdown.select_by_visible_text("Japan")
dropdown.select_by_value("JP")
dropdown.select_by_index(3)
# Get selected option
print(dropdown.first_selected_option.text)
# Get all options
for option in dropdown.options:
print(option.text, option.get_attribute("value"))JavaScript alerts:
# Accept an alert
alert = driver.switch_to.alert
print(alert.text) # Read alert message
alert.accept() # Click OK
# Dismiss (click Cancel)
alert.dismiss()
# Type into a prompt
alert.send_keys("my input")
alert.accept()New tabs/windows:
# Store original window handle
original_window = driver.current_window_handle
# Click link that opens a new tab
driver.find_element(By.LINK_TEXT, "Open in new tab").click()
# Switch to the new tab
WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) > 1)
new_window = [w for w in driver.window_handles if w != original_window][0]
driver.switch_to.window(new_window)
# Do work in new tab...
print(driver.title)
# Close new tab and switch back
driver.close()
driver.switch_to.window(original_window)Fix 7: Page Load and Network Timeouts
TimeoutException: Message: timeout: Timed out receiving message from rendererSet page load timeout:
driver.set_page_load_timeout(30) # Max 30 seconds for page load
try:
driver.get("https://slow-site.com")
except TimeoutException:
print("Page took too long to load")
driver.execute_script("window.stop();") # Stop loading, work with partial pageSet implicit wait (global default wait for find_element):
driver.implicitly_wait(5) # Wait up to 5s for any find_element call
# Applied to ALL find_element calls from this point onCommon Mistake: Mixing implicit waits and explicit waits (WebDriverWait). They can interact unpredictably — a 10s implicit wait plus a 10s explicit wait can result in up to 20s total wait time. Use explicit waits only for predictable behavior:
# WRONG — mixing implicit and explicit waits
driver.implicitly_wait(10)
WebDriverWait(driver, 10).until(EC.presence_of_element_located(...))
# CORRECT — explicit waits only, no implicit wait
WebDriverWait(driver, 10).until(EC.presence_of_element_located(...))Wait for AJAX to complete:
# Wait for jQuery AJAX calls to finish
WebDriverWait(driver, 30).until(
lambda d: d.execute_script("return jQuery.active == 0")
)
# Wait for network idle (no pending fetch requests)
WebDriverWait(driver, 30).until(
lambda d: d.execute_script(
"return window.performance.getEntriesByType('resource')"
".filter(r => !r.responseEnd).length === 0"
)
)Fix 8: Best Practices for Reliable Selenium Code
Always use a try/finally block or context manager:
from selenium import webdriver
# WRONG — if an error occurs, browser stays open (memory leak)
driver = webdriver.Chrome()
driver.get("https://example.com")
# ... error here, driver never quit
# CORRECT — always quit
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
# ... your code
finally:
driver.quit() # Closes browser AND frees driver processUse Page Object Model for maintainable tests:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
URL = "https://example.com/login"
USERNAME = (By.ID, "username")
PASSWORD = (By.ID, "password")
SUBMIT = (By.CSS_SELECTOR, "button[type='submit']")
ERROR_MSG = (By.CLASS_NAME, "error-message")
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def login(self, username, password):
self.driver.get(self.URL)
self.wait.until(EC.visibility_of_element_located(self.USERNAME)).send_keys(username)
self.driver.find_element(*self.PASSWORD).send_keys(password)
self.driver.find_element(*self.SUBMIT).click()
def get_error(self):
return self.wait.until(
EC.visibility_of_element_located(self.ERROR_MSG)
).text
# Usage
page = LoginPage(driver)
page.login("admin", "wrong_password")
assert "Invalid credentials" in page.get_error()Still Not Working?
Selenium vs Playwright vs Puppeteer
If you’re starting a new project:
- Playwright — modern, auto-wait, multi-browser, better async support. Recommended for new projects. See Playwright not working.
- Puppeteer — Node.js, Chrome-focused, good for scraping. See Puppeteer not working.
- Selenium — largest ecosystem, supports the most browsers and languages, most community resources.
Anti-Bot Detection
Many sites detect and block Selenium. Signs: immediate CAPTCHA, 403 responses, or empty pages. Selenium sets navigator.webdriver = true by default:
options = Options()
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
driver = webdriver.Chrome(options=options)
driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")Note: Bypassing bot detection may violate a site’s terms of service. Use responsibly and only on sites you have authorization to automate.
Selenium Grid for Parallel Tests
# Start Selenium Grid Hub
java -jar selenium-server-4.20.0.jar hub
# Start a node
java -jar selenium-server-4.20.0.jar node --hub http://localhost:4444from selenium import webdriver
options = webdriver.ChromeOptions()
driver = webdriver.Remote(
command_executor="http://localhost:4444/wd/hub",
options=options,
)For browser testing patterns with other frameworks, see Cypress not working.
Shadow DOM and Web Components
Modern sites built on Lit, Stencil, or Polymer hide UI inside closed shadow roots. Standard By.CSS_SELECTOR cannot cross those boundaries. Use the JavaScript bridge:
button = driver.execute_script("""
return document.querySelector('my-app')
.shadowRoot.querySelector('nav-bar')
.shadowRoot.querySelector('button#submit');
""")
button.click()If the host element is open, Selenium 4.10+ supports element.shadow_root.find_element(By.CSS_SELECTOR, "...") natively — but only for open roots. Closed roots always require JS.
Detached Browser Sessions in CI
CI runners (GitHub Actions, GitLab) sometimes leave orphan Chrome processes after a test crash. The next run inherits a stale user data directory and fails to start with user data directory is already in use. Always pass a unique data dir per run:
options.add_argument(f"--user-data-dir=/tmp/chrome-{os.getpid()}")Or clean /tmp/.org.chromium.* before each suite.
Network Throttling and Conditional Waits
If a flaky test only fails on slow connections, throttle deliberately to reproduce it:
driver.execute_cdp_cmd("Network.emulateNetworkConditions", {
"offline": False,
"latency": 200,
"downloadThroughput": 500 * 1024,
"uploadThroughput": 500 * 1024,
})Reproducible slow conditions surface race conditions that only appear in production traffic.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Scrapy Not Working — Spider Crawl Returns Nothing, Robots.txt Blocked, and Pipeline Errors
How to fix Scrapy errors — spider yields no items, robots.txt blocking all requests, 403 forbidden response, AttributeError on response.css, item pipeline not processing, AsyncIO reactor errors, and middleware not running.
Fix: freezegun Not Working — Datetime Not Frozen, Timezone Issues, and Async Tests
How to fix freezegun errors — freeze_time decorator not affecting datetime.now, timezone-aware datetime mismatch, time.time not frozen, async test time leak, third-party library still using real time, and tick parameter behavior.
Fix: Moto Not Working — Mock Decorator, Real AWS Calls Leaking, and v4 to v5 Migration
How to fix Moto errors — mock not activating, real AWS credentials used in tests, ImportError mock_s3 removed in v5, fixtures with multiple services, NoCredentialsError despite mock, and standalone server mode.
Fix: Nox Not Working — Session Errors, Virtualenv Backends, and Reuse Logic
How to fix Nox errors — no noxfile.py found, session not detected, virtualenv backend uv not installed, session.install fails outside virtualenv, parametrize matrix exploding, and reuse_venv confusion.