How to Find Broken Images With Selenium WebDriver
Find unavailable DOM images with Selenium by checking `complete` and `naturalWidth`. Handle lazy loading, dynamic pages, responsive sources, and common failure cases.
To find broken images with Selenium WebDriver, inspect each relevant img element after its load has settled. A practical browser-side failure signal is img.complete === true && img.naturalWidth === 0. The complete property alone does not mean an image loaded successfully: it is also true for broken images. A zero natural width means the browser has no intrinsic image width available; it does not tell you the HTTP status or the root cause.
The examples below report DOM images in the current browsing context. They do not automatically check CSS background images, unvisited frames, or images inside shadow roots. The Python example uses Selenium’s element lookup; the JavaScript-in-WebDriver example reads all images in one browser-side call.
Python: find broken images with Selenium
Install Selenium with python -m pip install selenium. Selenium Manager can manage supported browser drivers when you create a driver with a standard Selenium browser class. Make sure a compatible browser is available in the environment.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
url = "https://example.com"
driver = webdriver.Chrome()
try:
driver.get(url)
# This waits for currently present images to finish fetching. On dynamic
# pages, add an application-specific wait before collecting the image set.
WebDriverWait(driver, 20).until(
lambda d: d.execute_script(
"return Array.from(document.images).every(img => img.complete)"
)
)
broken = []
for image in driver.find_elements(By.TAG_NAME, "img"):
complete = image.get_property("complete")
natural_width = image.get_property("naturalWidth")
natural_height = image.get_property("naturalHeight")
src = image.get_attribute("src")
current_src = image.get_property("currentSrc")
if complete and natural_width == 0:
broken.append({
"src": src,
"currentSrc": current_src,
"naturalWidth": natural_width,
"naturalHeight": natural_height,
})
if broken:
print("Unavailable or failed images:")
for item in broken:
print(item)
else:
print("No settled broken images found.")
finally:
driver.quit()
The wait above only covers images already in document.images. It does not scroll to trigger lazy loading, and a page can add or replace images after the condition succeeds. For a page that renders images after an API request or user interaction, first wait for the site’s relevant content or state, then scan. For a quick diagnostic on a mostly static page, remove the explicit wait and classify only images where complete is true.
One browser-side scan with JavaScript
Using execute_script avoids one WebDriver command per image and returns structured records in one call. This JavaScript runs in the selected frame and window.
broken = driver.execute_script("""
return Array.from(document.images, img => ({
src: img.src,
currentSrc: img.currentSrc,
complete: img.complete,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight
})).filter(img => img.complete && img.naturalWidth === 0);
""")
for image in broken:
print(image)
For diagnostics, returning all images before filtering can be more useful: it lets you distinguish pending images from settled images that have no intrinsic data. Selenium’s official documentation covers [finding multiple elements](https://www.selenium.dev/documentation/webdriver/elements/finders/) and [executing JavaScript](https://www.selenium.dev/selenium/docs/api/javascript/IWebDriver.html).
Wait for the right page state
Selenium’s default normal page-load strategy waits for the document ready state to become complete. That is not a guarantee that a single-page application has finished rendering images: JavaScript can change the page after navigation returns. The eager strategy returns at interactive, while resources such as images may still load; none does not block on page loading. Choose the strategy to fit the test, then add explicit waits for the content you need. See [Selenium browser options](https://www.selenium.dev/documentation/webdriver/drivers/options/) and [waiting strategies](https://www.selenium.dev/documentation/en/webdriver/waits/).
Wait for currently known images
WebDriverWait(driver, 20).until(
lambda d: d.execute_script(
"return Array.from(document.images).every(img => img.complete)"
)
)
This condition can return immediately if there are no images yet. It can also succeed while the application is about to insert more. Pair it with a page-specific condition, such as the presence of the gallery container, an expected result count, or disappearance of a loading indicator.
Wait for an image set to stabilize
When the page inserts images in batches, compare the DOM image count over a short interval, then wait for those images to settle. A stable count is only a practical heuristic; an application-specific ready signal is better when available.
from selenium.webdriver.support.ui import WebDriverWait
previous_count = {"value": None, "stable": 0}
def image_count_stable(d):
count = d.execute_script("return document.images.length")
if count == previous_count["value"]:
previous_count["stable"] += 1
else:
previous_count["value"] = count
previous_count["stable"] = 0
return count if previous_count["stable"] >= 2 else False
WebDriverWait(driver, 15, poll_frequency=0.5).until(image_count_stable)
WebDriverWait(driver, 20).until(
lambda d: d.execute_script(
"return Array.from(document.images).every(img => img.complete)"
)
)
Lazy-loaded and responsive images
Lazy images may not begin loading until they approach the viewport. A scan performed before scrolling can therefore miss an image that has not yet been requested. Scroll through the page, allowing each region to enter view, then wait for the resulting images to settle before classifying them.
driver.execute_script("""
const step = Math.max(400, window.innerHeight);
let y = 0;
while (y < document.body.scrollHeight) {
window.scrollTo(0, y);
y += step;
}
window.scrollTo(0, 0);
""")
WebDriverWait(driver, 20).until(
lambda d: d.execute_script(
"return Array.from(document.images).every(img => img.complete)"
)
)
Some pages extend their height as you scroll. For those, use a loop that rechecks document.body.scrollHeight and stops only after the bottom remains stable, with a maximum duration to prevent an endless scroll. Scrolling the whole page can also trigger analytics, infinite feeds, or expensive content loads, so restrict it to the page regions your test is meant to cover.
Responsive images can have a src fallback and a different browser-selected currentSrc, chosen from srcset and sizes. Log both values. The browser’s actual selection is usually the useful URL to investigate for a particular viewport.
What counts as a broken image?
| Browser properties | Interpretation | Suggested handling |
|---|---|---|
complete === false |
Fetching has not settled, or the image has not started loading. | Wait, scroll to trigger lazy loading, or report as pending rather than failed. |
complete === true and naturalWidth > 0 |
Intrinsic image data is available. | Usually treat as loaded for this check. |
complete === true and naturalWidth === 0 |
No intrinsic width is available. This is a useful signal for a failed or unavailable image. | Record it for investigation; do not infer a specific HTTP status or cause from these properties. |
MDN documents that complete can be true for a broken image, and that naturalWidth is zero when intrinsic width is unavailable. See [HTMLImageElement.complete](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/complete) and [HTMLImageElement.naturalWidth](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalWidth).
Coverage boundaries and edge cases
- Images inserted after the scan: wait for an application-specific signal or rescan after the relevant interaction.
- Lazy loading: scroll relevant content into view before checking.
- Responsive sources: record
currentSrcas well as the declaredsrc. - CSS backgrounds:
document.imagesdoes not include CSSbackground-imageresources. Inspect computed styles separately if they are in scope; determining whether each background resource decoded successfully needs additional checks. - Frames: the scan covers the currently selected browsing context. Switch into each relevant iframe and scan it separately; account for cross-origin navigation and frame availability.
- Shadow DOM: images inside shadow roots may not be included in
document.images. Traverse open shadow roots if required; closed roots are not exposed for ordinary page-script traversal. - Intentional placeholders: a transparent tracking pixel or deliberate empty image may be technically valid. Apply site-specific exclusions rather than labeling every zero-width case a defect.
- Broken but visually hidden images: decide whether your requirement covers all DOM images or only visible content. Visibility filtering changes the meaning of the report.
Turn the scan into a useful test result
Keep the detection step separate from the policy for passing or failing a test. Collect diagnostic fields first, then decide whether any match should fail the test. This makes it easier to allow known placeholders, exclude third-party images, or report pending loads separately.
records = driver.execute_script("""
return Array.from(document.images, img => ({
src: img.src,
currentSrc: img.currentSrc,
alt: img.alt,
complete: img.complete,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight
}));
""")
failed = [
image for image in records
if image["complete"] and image["naturalWidth"] == 0
]
pending = [image for image in records if not image["complete"]]
print({"failed_or_unavailable": failed, "pending": pending})
if failed:
raise AssertionError(f"Found {len(failed)} failed or unavailable images")
For repeatable results, record the URL, browser viewport, selected frame, time of scan, src, currentSrc, and image dimensions. The same responsive page can select different sources at different viewport sizes. Avoid claiming a network status from these DOM properties alone; use browser network logs or a separate HTTP check if you need response-level diagnostics.
Performance, reliability, and cost
Reading each property through WebDriver is straightforward, but it requires several remote commands per image. A single execute_script call returns the same kind of records with fewer WebDriver round trips, which is useful on pages with many images. Scrolling lazy content and waiting for dynamic pages adds time; scope the check to relevant content and use explicit conditions rather than a long fixed sleep.
Reliability depends on defining when the page is ready for this check. Navigation completion alone may not cover JavaScript-rendered content, and waiting for all images can take a long time on pages with trackers or endless feeds. Set bounded waits, report pending entries separately, and make the test’s coverage explicit. Selenium itself has no per-image charge; the costs to consider are your browser and CI compute, execution time, and any external resources the page loads.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No broken images are reported, but the page visibly has one. | The scan ran before loading settled, used a different responsive source, or did not cover the relevant frame or shadow root. | Wait for the page state, log currentSrc, use the target viewport, and inspect the relevant context. |
The wait times out on complete. |
An image is still loading, a request is stalled, or the page keeps adding images. | Use a bounded wait, inspect pending records, and wait for the application’s content state before checking images. |
| The scan reports a zero-width image that appears intentional. | The page uses a placeholder, tracking pixel, or other image without intrinsic dimensions. | Apply a documented site-specific exclusion and retain the record for diagnostics. |
| Lazy images are missing from the report. | They have not entered the viewport, so loading has not started. | Scroll the relevant regions into view, then wait and rescan. |
find_elements returns an empty list. |
No matching image elements exist in the current context at that moment. | Check the locator and frame, then wait for the component that creates the images. Selenium plural finders return an empty list when there are no matches. |
| A browser or driver session fails to start. | The browser is missing, incompatible, or unavailable to the automation environment. | Install a supported browser, check the environment’s driver setup, and review Selenium’s browser-specific setup guidance. |
Or skip the browser setup
If you need a clean visual capture alongside a browser-based QA workflow, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns an image or PDF, and the API accepts parameter names used by other screenshot APIs. The [API documentation](https://screenshotneo.com/docs/) lists the available options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Replace YOUR_API_KEY with your key. In Node.js environments without Bun.write, write the response bytes with the environment’s standard file API. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does a broken image always have an HTTP 404 status?
No. The DOM properties identify unavailable intrinsic image data, not the server’s response status or the reason loading failed.
Can Selenium check images without downloading them again?
The browser properties describe the image elements already loaded or attempted in that browser context. A separate HTTP check may make another request and can behave differently due to cookies, headers, caching, or responsive selection.
Should an image with naturalWidth equal to zero always fail the test?
Only if that matches your test policy. Some pages intentionally use empty or placeholder images, so define exclusions for the site under test.


