Selenium Screenshot vs Screenshot API
Selenium is the most widely used browser testing framework in the world, with an estimated 55,000+ companies running it in production. It can also take screenshots. Those two facts get conflated constantly, and the result is teams running a full testing stack just to generate PNGs. This page breaks down where Selenium screenshots work fine, where they fall apart, and when a dedicated screenshot API saves more time than it costs.
Why Selenium screenshots get painful over time
Selenium is a testing framework with a screenshot method. That's not a criticism, it's just the architecture. The tool was designed to drive browsers through test scenarios: click this button, fill that form, assert this element exists. Taking a screenshot is a side feature, and the gaps show up fast once screenshots become a core part of your workflow.
The first thing you hit is the driver version dance. Chrome auto-updates every four to six weeks. When it does, your ChromeDriver stops matching and Selenium throws a SessionNotCreatedException. Selenium Manager (added in v4.6) helps by auto-downloading drivers, but it doesn't solve everything. In Docker containers, in CI runners, on locked-down enterprise machines, the mismatch still bites. GitHub issue #1944 on the docker-selenium repo has been a recurring thread for years.
Then there's the full-page problem. Selenium's getScreenshotAs() captures the visible viewport. That's it. If you need the whole page, Firefox has getFullPageScreenshotAs() but Chrome does not. Issue #1141 on the Selenium repo has been open since 2016 with no universal fix. The workarounds aren't pretty: resize the window to body.scrollHeight (breaks on lazy-loaded content), use the AShot library in Java (duplicates sticky headers), or send CDP commands through BiDi (complex and poorly documented for most teams).
Output format is another limit. Selenium produces PNG. No JPEG, no WebP, no quality control or format selection, no PDF. If you need a different format, you're pulling in Pillow or ImageMagick after the fact.
Bot detection is getting worse too. Major sites actively fingerprint Selenium through the navigator.webdriver property, missing browser plugins, and headless-mode giveaways. Adobe, Nike, and dozens of others will serve you a blank page or a CAPTCHA instead of the content you're trying to screenshot. There are stealth plugins and undetected-chromedriver forks, but they're community-maintained workarounds in a cat-and-mouse game that never ends. Every few months, detection techniques evolve and the workarounds break again.
Headless mode has its own set of problems. Screenshots taken in headless Chrome don't always match what you see in a headed browser. Viewport sizing behaves differently, font rendering varies, and some CSS animations don't trigger at all. Chrome shipped --headless=new starting with version 109, which improved rendering consistency, but there are still edge cases with DPI scaling and GPU compositing that produce slightly different output on different machines. If you've ever had a screenshot pass locally and fail in CI, this is usually why.
And the resource cost adds up. Each Chromium instance pulls somewhere between a quarter and three quarters of a gigabyte of RAM, depending on what the page loads. Run ten concurrent captures on an 8 GB server and you're one heavy page away from an OOM kill. I've seen teams allocate entire EC2 instances just for screenshot jobs that could have been a few API calls. That felt like overkill at the time, and looking back, it absolutely was.
What does a screenshot API actually replace?
A screenshot API replaces the browser layer. Instead of launching Chromium locally, navigating to a URL, waiting for content, capturing the viewport, and cleaning up the process, you send an HTTP request with your parameters and get back an image. The API runs Chromium on its own infrastructure, handles the rendering, and returns the result.
That sounds abstract, so here's what it looks like in practice. A typical Selenium screenshot in Python:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = Options()
options.add_argument('--headless=new')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--window-size=1280,800')
driver = webdriver.Chrome(options=options)
try:
driver.get('https://example.com')
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, '.main-content'))
)
# Full-page workaround (Chrome has no native full-page method)
total_height = driver.execute_script(
'return document.body.scrollHeight'
)
driver.set_window_size(1280, total_height)
driver.save_screenshot('screenshot.png')
finally:
driver.quit()
Twenty-five lines, and this version skips error handling, retry logic, cookie banner dismissal, and the ChromeDriver version check that would double the code in production. The full-page workaround resizes the window to the page height, which doesn't work on pages with lazy-loaded images or infinite scroll.
The same result with an API call:
import requests
response = requests.get(
'https://api.screenshotrun.com/v1/screenshots/capture',
params={
'url': 'https://example.com',
'width': '1280',
'height': '800',
'full_page': 'true',
'format': 'png',
'wait_for_selector': '.main-content'
},
headers={'Authorization': 'Bearer YOUR_API_KEY'}
)
with open('screenshot.png', 'wb') as f:
f.write(response.content)
Fifteen lines. No browser binary, no driver, no window resize hack. Full-page capture is a parameter, not a workaround. The API handles lazy-loaded content, cookie banners (block_cookies=true), and ad overlays (block_ads=true) without extra code.
For Java teams, the contrast is even sharper. Here's the Selenium version:
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.*;
import org.openqa.selenium.support.ui.*;
import java.io.*;
import java.nio.file.*;
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new", "--no-sandbox",
"--disable-dev-shm-usage", "--window-size=1280,800");
WebDriver driver = new ChromeDriver(options);
try {
driver.get("https://example.com");
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.presenceOfElementLocated(
By.cssSelector(".main-content")));
// Full-page: Firefox-only method, Chrome needs workaround
long height = (long) ((JavascriptExecutor) driver)
.executeScript("return document.body.scrollHeight");
driver.manage().window().setSize(new Dimension(1280, (int) height));
File src = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.FILE);
Files.copy(src.toPath(),
Path.of("screenshot.png"),
StandardCopyOption.REPLACE_EXISTING);
} finally {
driver.quit();
}
That TakesScreenshot cast is something every Java Selenium developer has typed a hundred times. The API equivalent is a standard HttpClient GET request, nothing Selenium-specific at all.
And here's the part that matters most for teams with mixed stacks: the API is just HTTP. You don't need Python bindings or Java bindings or any bindings. A cURL one-liner in a deploy script, a fetch() call in a serverless function, a file_get_contents() in PHP, it all works the same way. Selenium ties you to one of its supported languages. The API doesn't care what language your backend runs.
For CI/CD pipelines specifically, the difference is dramatic. A Selenium screenshot step in GitHub Actions means installing Chrome, installing ChromeDriver (or pulling a 1+ GB Docker image like selenium/standalone-chrome), running your script, and hoping the versions still match. That step alone can add two to three minutes to your pipeline. With an API call, it's a curl command that finishes in under two seconds. No Docker image, no browser install, no artifact storage for PNGs. I switched a client's deploy-verification screenshots from Selenium to API calls last year and their pipeline went from nine minutes to under four.
Side-by-side comparison
The table below maps every Selenium screenshot limitation to the corresponding API capability. Some of these (like format selection) might seem minor on their own, but they compound. Maintaining workarounds for five or six of these at once is what turns a "simple screenshot feature" into a maintenance project.
| Capability | Selenium WebDriver | ScreenshotRun API |
|---|---|---|
| Primary purpose | Browser testing and automation | Screenshot and PDF generation |
| Setup | Browser + driver + language bindings | HTTP request with API key |
| Languages | Python, Java, C#, Ruby, JS | Any language with HTTP support |
| Full-page capture | Firefox only (Chrome needs workarounds) | full_page=true |
| Output formats | PNG only | PNG, JPEG, WebP, AVIF, TIFF, PDF |
| Dark mode | Manual CSS injection | dark_mode=true |
| Retina / HiDPI | Manual devicePixelRatio config |
retina=true |
| Cookie banner blocking | Custom selector lists per framework | block_cookies=true |
| Ad blocking | Not built in | block_ads=true |
| RAM per capture | 300-800 MB (local Chromium) | None (server-side) |
| Driver version management | Required (Chrome updates break drivers) | None |
| Maintenance | Ongoing (driver sync, Docker images, process management) | None |
| Cost | Free (self-hosted) + engineering time | Free tier (200/month), paid plans after |
For teams that used Selenium from Python, the Python integration page shows every parameter. For Java shops, the Java integration has the same coverage with HttpClient examples. Both take about five minutes to wire up.
A few things the table doesn't capture: Selenium has no built-in caching. If you screenshot the same URL twice, it launches Chrome twice. ScreenshotRun's cache_ttl parameter returns the cached image on repeat requests, which matters when the same page gets requested by multiple users or services. There's also webhook delivery: instead of polling for the result, the API can POST the finished image to your endpoint when it's ready. Handy for async pipelines where you don't want to hold a connection open.
When should you keep using Selenium for screenshots?
Selenium is the right choice in several real scenarios, and swapping it for an API there would be a mistake.
If screenshots are test artifacts for visual regression testing or failure evidence, keep them in Selenium. You're already running the browser for assertions, and getScreenshotAs() captures the exact state your test sees. Adding an API call in the middle of a test flow adds latency and a network dependency that makes the test flakier, not better. The whole point is capturing what the test browser rendered.
If you need multi-step browser interaction before the screenshot, log in through a form, navigate a wizard, fill fields, click through dialogs, then Selenium's automation is doing real work. A screenshot API can handle simple auth via cookies and headers parameters, but complex multi-page flows need a real browser session.
There are also infrastructure constraints worth considering. If you specifically need Firefox or WebKit rendering, Selenium Grid gives you real cross-browser screenshots that a Chromium-only API can't match. And teams operating in air-gapped environments or under strict compliance rules where URLs can't leave the network have no choice but to self-host. Those are hard constraints, not preferences.
The break-even point is this: if screenshots are a byproduct of testing, Selenium is already there and it's free. If screenshots are a feature (thumbnails, previews, reports, monitoring, OG images), you're running an entire testing framework as screenshot infrastructure. That's where the API simplifies things.
The real cost isn't the API fee
When developers compare Selenium (free) to an API (paid), they're comparing the sticker price. The real cost of Selenium screenshots is everything around the screenshot method itself.
There's the ChromeDriver version sync that breaks after every Chrome update. The Docker image that needs rebuilding when Selenium or Chrome releases a new version. The memory tuning when concurrent captures push past what the server can handle. The flaky CI screenshots that pass locally but produce blank PNGs on GitHub Actions because the runner's headless Chrome renders differently. I talked to a team last year that had three Jira tickets open just for "screenshot pipeline broken again" — all caused by Chrome auto-updating on the CI runner while ChromeDriver stayed pinned to the old version.
Selenium is actively maintained, regularly updated, and isn't going anywhere. But "it can take screenshots" and "it's a good screenshot tool" are different statements.
The hybrid approach works well in practice: keep Selenium for your test suite where it belongs, and use the API for production screenshot features. Your QA team keeps their tooling. Your product team gets reliable screenshots without filing tickets every time Chrome updates. Nobody has to maintain a screenshot-specific Selenium Grid alongside the testing one.
For teams where screenshots are a feature rather than a test artifact, an API built for that specific job tends to cost less in total when you factor in engineering time, even with the monthly fee. ScreenshotRun's free tier covers 200 screenshots per month, enough to test the switch before committing. The Puppeteer and Playwright comparison pages cover the same trade-off for those frameworks if your stack uses either one instead.
Frequently asked questions
Only in Firefox, which has a native getFullPageScreenshotAs() method. Chrome and Edge capture only the visible viewport. Workarounds include resizing the browser window to the full page height, using the AShot library in Java, or sending CDP commands. All of these have limitations with lazy-loaded content and sticky elements. A screenshot API handles full-page capture with a single full_page=true parameter.
Selenium was designed for browser testing, not screenshot generation. It works for capturing test artifacts, but using it as production screenshot infrastructure means managing ChromeDriver versions, Chromium memory, Docker images, and process lifecycle. For production use cases like thumbnails, previews, or reports, a dedicated screenshot API is simpler to operate and scale.
Each Chromium instance launched by Selenium consumes roughly 300 to 800 MB of RAM depending on page complexity. Running ten concurrent captures on an 8 GB server can trigger out-of-memory kills. A screenshot API offloads all rendering to remote infrastructure, using no local memory.
Selenium produces PNG screenshots only. There is no built-in option for JPEG, WebP, AVIF, or PDF output. If you need a different format, you have to convert the PNG after capture using a library like Pillow or ImageMagick. Screenshot APIs typically support multiple formats from a single endpoint.
Yes, and many teams do. The hybrid approach keeps Selenium for test suites where it captures the exact browser state during assertions, while using a screenshot API for production features like link previews, OG images, or scheduled monitoring. This avoids maintaining separate screenshot infrastructure alongside the testing setup.
Vitalii Holben