How to Capture a Website Screenshot with Selenium and ChromeDriver in Rust
Use Rust’s thirtyfour WebDriver client to open Chrome, wait for a page, save a PNG screenshot, and close the browser cleanly.
Use thirtyfour, an asynchronous Rust client for the W3C WebDriver protocol, to control Chrome through ChromeDriver and save the returned PNG bytes. The shortest path is WebDriver::managed: it downloads and starts a matching local ChromeDriver for a compatible installed Chrome. Navigate to the page, wait for a meaningful element, capture the screenshot, write the bytes to disk, and call quit() to close the session. The current thirtyfour documentation lists browser and element PNG screenshots, Tokio async support, and direct or managed WebDriver sessions.
This is Rust using a Selenium-compatible WebDriver client; the research sources reviewed do not identify an official Selenium Rust language binding. The example below pins thirtyfour 0.37.5 and uses its browser screenshot method. Confirm the API against the version you pin if you change versions.
1. Create a Rust project and capture a PNG
Install Rust with Cargo, then create a binary project:
cargo new rust-site-shot
cd rust-site-shot
Replace Cargo.toml with these dependencies. Tokio’s macros and rt-multi-thread features support the async entry point; anyhow lets the example propagate both WebDriver and file errors.
[package]
name = "rust-site-shot"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
thirtyfour = "=0.37.5"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Put this in src/main.rs:
use anyhow::Result;
use std::fs;
use thirtyfour::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
// Requires a compatible Chrome installation. The managed client
// downloads and starts the matching local ChromeDriver.
let caps = DesiredCapabilities::chrome();
let driver = WebDriver::managed(caps).await?;
let capture_result: Result<()> = async {
driver.goto("https://example.com").await?;
// Wait for content that matters instead of guessing with a sleep.
driver
.query(By::Css("h1"))
.desc("example.com page heading")
.single()
.await?;
// WebDriver returns PNG bytes for the current browser context.
let png = driver.screenshot().await?;
fs::write("screenshot.png", png)?;
println!("Saved screenshot.png");
Ok(())
}
.await;
// Quit even when navigation, waiting, capture, or writing failed.
let quit_result = driver.quit().await;
capture_result?;
quit_result?;
Ok(())
}
Run it with cargo run. The file is written relative to the process’s current working directory. Create a parent directory first if you change the output path. The first managed run may need network access to obtain ChromeDriver.
What the screenshot contains
The ordinary WebDriver screenshot captures the current browsing context, typically the visible browser viewport. Do not assume it produces a full-page image: the supplied Selenium overview describes the screenshot endpoint but does not establish full-page behavior for this Rust call. For a particular element, thirtyfour documents element screenshots too; look up the pinned version’s WebElement screenshot method and save its returned PNG bytes. For full-page capture, check the browser and driver protocol support you plan to use and verify the output dimensions rather than treating a viewport capture as full page.
2. Choose how ChromeDriver is managed
| Approach | Use it when | Trade-off |
|---|---|---|
WebDriver::managed(caps) |
You want the Rust client to manage a local matching ChromeDriver. | Convenient for local scripts; the environment must permit downloading and caching the driver and must have compatible Chrome installed. |
| External WebDriver endpoint | You already run ChromeDriver, Selenium Standalone, or a Grid. | You control deployment and versions, but must operate the service and configure its endpoint. |
The thirtyfour documentation says its managed session downloads a matching ChromeDriver, starts it locally, and shuts the process down when the session is dropped. Still call quit(): the crate warns that relying on destruction can block the async executor and hide shutdown errors. For an externally managed process, the crate documents WebDriverBuilder for connecting to a manually launched driver or Grid.
Manual ChromeDriver setup
- Install Chrome or Chromium in a location ChromeDriver recognizes, or configure its browser binary path through Chrome options.
- Download the ChromeDriver executable for your platform and keep its version compatible with the installed browser.
- Make the executable discoverable, commonly by adding its directory to
PATH. - Start the driver service yourself, then connect the Rust client to its WebDriver endpoint using the external-driver builder API documented for your pinned thirtyfour version.
ChromeDriver is a separate executable used by Selenium WebDriver to control Chrome. Its official setup instructions cover installing the browser, obtaining the driver, and making it discoverable through PATH. See ChromeDriver’s setup guide and ChromeOptions and capabilities for platform and browser-path details. The exact builder method and endpoint configuration depend on your pinned thirtyfour release and deployment.
Selenium Manager is a separate driver-management facility documented by Selenium bindings. Do not assume that a Rust client automatically invokes Selenium Manager: thirtyfour documents its own managed local-driver path and separately supports externally managed WebDriver services. Selenium Manager documentation describes its driver discovery, download, and cache behavior.
3. Make capture timing reliable
Navigation completing does not necessarily mean that the exact content you need has rendered. Modern pages may hydrate after initial HTML, load images lazily, or fetch data after navigation. Wait for an element that indicates readiness:
driver
.query(By::Css("main article h1"))
.desc("article heading")
.single()
.await?;
let png = driver.screenshot().await?;
query polls until its condition is met and is the crate’s recommended query interface. Prefer stable selectors you own, such as a test ID, and use selectors exposed by third-party pages when automating them. A fixed sleep is simple but can be too short on a slow response and unnecessarily long on a fast one.
If the page changes after the first ready element appears, wait for the relevant state: a result row, a chart container, an image, or a known loading indicator to disappear. Keep the wait bounded by the crate’s configured timeout so a broken page does not leave the job running forever. Avoid using “network idle” as a universal definition of readiness: sites with analytics, streaming, or long-polling connections may never become idle.
4. Adapt the capture for real pages
Use a different target URL
Replace https://example.com with the target URL. Include the scheme (https:// or http://) and ensure the machine running Chrome can reach it. Redirects are followed by the browser; wait for a selector on the final page if the redirect target renders asynchronously.
Save to a chosen path
fs::create_dir_all("output")?;
fs::write("output/page.png", png)?;
File writes are local to the Rust process. In a container, CI job, or service, choose a writable mounted directory and upload or retain the artifact through that environment’s normal mechanism.
Capture a particular element
The crate supports PNG capture for a browser or an individual element. Locate the element with a stable CSS selector or test ID, then use the element screenshot API documented for your pinned crate version. This is useful for a chart or card where surrounding navigation is irrelevant. Element capture can fail when the selector matches nothing, the element becomes stale after a rerender, or the element is outside conditions supported by the browser driver.
Set browser capabilities and viewport
DesiredCapabilities::chrome() creates Chrome capabilities. Add Chrome options only for a concrete need, such as a non-default browser binary or a controlled viewport. The supported capability-building methods can vary across crate versions; consult the thirtyfour and ChromeDriver capabilities documentation for the exact option names. A screenshot’s viewport affects responsive breakpoints and visible content, so set it deliberately when visual consistency matters. Do not add a universal list of headless or container flags: runtime requirements depend on the host image, Chrome build, and deployment environment.
Browser state, authentication, and consent
A newly created browser session usually has its own cookies and storage. If a target requires login, provide authorized test credentials and establish the session before capture; never place secrets in source control or print them in logs. Cookie consent banners can cover page content. In a test you control, set the intended consent state through the application’s supported test setup or interact with its banner before taking the screenshot. Third-party pages may show CAPTCHA or bot checks, which should be treated as a failed or blocked capture rather than as the intended page.
5. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Session creation reports a Chrome/ChromeDriver version mismatch | The browser updated while a manually managed driver remained at an older version. | Use WebDriver::managed where suitable, or update and pin browser and driver versions together. Selenium Manager documents this version-drift failure mode for Selenium bindings. |
| Driver executable not found or cannot start | ChromeDriver is absent, not executable, or not discoverable by PATH; browser installation may also be missing. | Install Chrome/Chromium and the platform-matching ChromeDriver, confirm PATH and executable permissions, or use the managed path where downloads are permitted. |
| Managed driver setup fails before opening Chrome | The host may block downloads, lack network access, or have no compatible Chrome installation. | Check the runtime’s outbound access and driver cache permissions. In restricted builds, provision Chrome and a driver explicitly and use an external WebDriver service. |
| Connection refused to WebDriver | The external ChromeDriver or Grid is not running, or the client endpoint/port is wrong. | Start the service, confirm its listening address from the same runtime environment, and configure the client to use that reachable endpoint. |
| Element query times out | The selector is wrong, content has not loaded, the page redirected, or a login/interstitial replaced the expected page. | Inspect the final URL and page state, choose a selector that exists in the rendered page, and wait for the actual readiness condition. |
| Screenshot is blank, incomplete, or shows a loading state | Capture occurred before client-side rendering, lazy loading, or image decoding completed. | Wait for the page-specific content or image state. If capturing below the fold, scroll through the relevant area to trigger lazy loading, then verify the browser and driver support the desired capture scope. |
| PNG file is missing or empty | The process lacks write permission, the output directory does not exist, or an error occurred before file writing. | Create the parent directory, choose a writable path, propagate file errors, and check that the screenshot bytes were produced before writing. |
| Chrome exits immediately in CI or a container | The browser environment lacks required runtime dependencies or has incompatible launch configuration. | Use a CI image with its browser dependencies installed, check ChromeDriver’s troubleshooting guidance, and configure Chrome options for that specific environment. |
| Browser processes remain after the job | The program exits on an earlier error without calling quit(). |
Structure cleanup to run after both success and failure, as in the example. For more complex code, use a scope/cleanup pattern that preserves the original capture error while still recording a quit error. |
When debugging, log the target URL (without credentials or sensitive query parameters), wait stage, browser and driver versions, and the WebDriver error. Avoid retrying every failure blindly: retry transient navigation or service errors with a limit, but a deterministic selector error or authentication failure needs a fix.
6. Performance, reliability, and cost
- Startup: Creating a browser session and starting ChromeDriver adds work before navigation. For a small one-off script, simplicity matters; for repeated captures, reuse a session where isolation requirements allow it, or use a long-lived external service.
- Memory: A full browser is heavier than a direct HTTP request. Limit concurrent browser sessions to what the machine can support and close each session after use.
- Determinism: Pin the Rust crate and, in reproducible CI, coordinate Chrome and ChromeDriver versions. Use the same viewport, locale, time zone, fonts, and authenticated state when screenshot comparisons need consistent rendering.
- Timeouts and retries: Bound navigation and element waits. Retry only transient failures with a finite policy; do not repeatedly retry a blocked page or an invalid selector.
- Cost: The Rust crates are dependencies, but browser execution consumes your own compute, storage, and maintenance time. A managed local driver can reduce manual setup; a remote Grid shifts browser operations to that service.
- Scope: This method produces PNG screenshots through Chrome. If the required artifact is a PDF, element-only image, full page, or many URLs, verify the specific browser/protocol support and operational requirements before building around assumptions.
Or skip the browser setup
If you need website screenshots without provisioning Chrome and ChromeDriver, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie/consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status in headers. AI agents can use its MCP server tools: take_screenshot, get_page_info, and capture_pdf.
For example, save a WebP screenshot with cURL:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python equivalent:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js equivalent:
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);
For Node.js environments without Bun, write the response bytes with your runtime’s filesystem API. See the ScreenshotNeo API documentation for parameters and response details. The API also supports full-page capture, CSS selectors, device presets, custom headers, cookies, JavaScript, PDF options, asynchronous jobs, and bulk capture. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account to get an API key and try the first 1,000 shots.
FAQ
Is there an official Selenium binding for Rust?
The sources reviewed do not list an official Rust Selenium binding. thirtyfour is a Rust WebDriver client that implements W3C WebDriver v1 and works with Chrome among its tested browsers.
Does this example create a full-page screenshot?
Do not assume so. The standard screenshot call captures the current browsing context; full-page behavior is not established by the cited material for this Rust method. Confirm support for your chosen browser and driver, or use a capture API whose full-page option is documented.
Can I use Selenium Grid instead of local ChromeDriver?
Yes. thirtyfour documents sessions through Selenium Standalone or Grid, as well as direct sessions. Configure the client for the externally managed endpoint using the API for your pinned crate version.
Why should I call quit() if the process is about to exit?
Because thirtyfour warns that fallback destruction may block the async executor and suppress shutdown errors. An explicit async quit makes cleanup observable.
Can I capture a page that requires authentication?
Yes, if you can establish an authorized browser session first. Supply test credentials securely, complete the login flow or set appropriate cookies, and wait for an authenticated page element before capture.


