ScreenshotNeo

BlogHow-to

How to Take Selenium Screenshots in Parallel Without File Conflicts

Give every parallel test a unique screenshot path. Here are runnable Selenium patterns for Python, Java, C#, Ruby, and JavaScript, plus Grid and CI guidance.

By the ScreenshotNeo team4 October 202610 min read

Give every test execution its own screenshot destination. A shared filename such as screenshot.png can be overwritten or raced over when parallel workers save at the same time. Selenium lets your code choose the filename; it does not impose a parallel screenshot naming convention. Build a unique path from a CI run identifier, a sanitized test name or ID, and worker or retry identity.

For example: screenshots/<run-id>/<test-id>-worker-<worker>-attempt-<attempt>.png. Create the parent directory, check that the save succeeded, and configure CI to collect that directory. Selenium’s screenshot examples and API document caller-provided destinations and screenshot data. Selenium screenshot examples · Python WebDriver API.

1. Why parallel screenshots collide

Each WebDriver session captures its current browsing context. But when multiple test processes write to the same filesystem path, ordinary filesystem writes can replace or race over the same file. The result may be one worker’s image replacing another’s, an incomplete artifact, or an artifact whose name no longer identifies the test that produced it.

Separate browser sessions help isolate browser state; they do not make a shared output filename unique. Selenium Grid routes WebDriver scripts to remote browser instances and supports parallel execution, while naming and collecting screenshot artifacts remain responsibilities of the test code and CI setup. Selenium Grid documentation.

2. Choose a unique artifact path

Use identifiers that exist in your test harness and are stable enough to diagnose failures. A useful set is:

  • Run ID: CI build, pipeline, or test-run identifier. It separates artifacts from different executions.
  • Test ID: a scenario name, test case ID, or parameterized test identifier.
  • Worker or shard ID: distinguishes concurrently running workers.
  • Attempt or retry ID: keeps a retry from replacing the first attempt’s evidence.

Sanitize values before using them as path components: replace slashes and other path separators, trim whitespace, and limit length. If a field can be missing, provide a stable fallback such as worker-unknown. Do not rely on timestamps alone when deterministic run and test identifiers are available. This naming recipe is practical implementation guidance, not a Selenium-mandated convention.

3. Python: save to a unique path and check the result

The Python binding provides save_screenshot(filename) and get_screenshot_as_file(filename). The file-saving method returns a Boolean, so check it instead of assuming the artifact exists.

from pathlib import Path
import re
from selenium import webdriver


def safe_component(value, fallback):
    value = str(value or fallback)
    value = re.sub(r"[^A-Za-z0-9._-]+", "_", value).strip("._-")
    return (value or fallback)[:100]


def screenshot_path(run_id, test_id, worker_id, attempt):
    directory = Path("artifacts/screenshots") / safe_component(run_id, "run-unknown")
    directory.mkdir(parents=True, exist_ok=True)
    filename = (
        f"{safe_component(test_id, 'test-unknown')}"
        f"-worker-{safe_component(worker_id, '0')}"
        f"-attempt-{safe_component(attempt, '1')}.png"
    )
    return directory / filename

# Supply these values from your test runner or CI environment.
run_id = "build-4812"
test_id = "checkout_guest_user"
worker_id = "3"
attempt = "1"

options = webdriver.ChromeOptions()
# options.add_argument("--headless=new")  # Optional in environments that support it.
driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")
    destination = screenshot_path(run_id, test_id, worker_id, attempt)
    saved = driver.save_screenshot(str(destination))
    if not saved:
        raise IOError(f"WebDriver did not save screenshot to {destination}")
    print(f"Saved {destination}")
finally:
    driver.quit()

Use your runner’s actual worker and retry identifiers. If your framework already provides a unique test execution ID, prefer it over reconstructing identity from a display name.

4. Other Selenium bindings

The same rule applies in each language: create the destination directory, include execution identity in the filename, and handle save failures. These snippets show the screenshot-saving part; provide runId, testId, workerId, and attempt from the runner.

Java

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;

static String safe(String value) {
    String cleaned = value == null ? "unknown" : value.replaceAll("[^A-Za-z0-9._-]+", "_");
    return cleaned.replaceAll("^[._-]+|[._-]+$", "");
}

static Path saveScreenshot(WebDriver driver, String runId, String testId,
                           String workerId, String attempt) throws Exception {
    Path dir = Paths.get("artifacts", "screenshots", safe(runId));
    Files.createDirectories(dir);
    Path target = dir.resolve(safe(testId) + "-worker-" + safe(workerId)
                               + "-attempt-" + safe(attempt) + ".png");
    Path actual = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE).toPath();
    return Files.copy(actual, target); // Use unique inputs; handle an existing target as an error.
}

If the destination may already exist, decide explicitly whether to fail, replace, or add another unique execution identifier. Avoid silently replacing an artifact that you need for diagnosis.

C#

using System.IO;
using System.Text.RegularExpressions;
using OpenQA.Selenium;

static string Safe(string value) {
    var cleaned = Regex.Replace(value ?? "unknown", @"[^A-Za-z0-9._-]+", "_").Trim('.', '_', '-');
    return string.IsNullOrEmpty(cleaned) ? "unknown" : cleaned;
}

static string SaveScreenshot(IWebDriver driver, string runId, string testId,
                             string workerId, string attempt) {
    var dir = Path.Combine("artifacts", "screenshots", Safe(runId));
    Directory.CreateDirectory(dir);
    var path = Path.Combine(dir, $"{Safe(testId)}-worker-{Safe(workerId)}-attempt-{Safe(attempt)}.png");
    driver.GetScreenshot().SaveAsFile(path);
    return path;
}

Ruby

require "fileutils"
require "selenium-webdriver"

def safe_component(value)
  cleaned = (value || "unknown").to_s.gsub(/[^A-Za-z0-9._-]+/, "_").gsub(/\A[._-]+|[._-]+\z/, "")
  cleaned.empty? ? "unknown" : cleaned[0, 100]
end

def save_screenshot(driver, run_id:, test_id:, worker_id:, attempt:)
  dir = File.join("artifacts", "screenshots", safe_component(run_id))
  FileUtils.mkdir_p(dir)
  path = File.join(dir, "#{safe_component(test_id)}-worker-#{safe_component(worker_id)}-attempt-#{safe_component(attempt)}.png")
  driver.save_screenshot(path)
  path
end

JavaScript (Node.js)

Selenium’s JavaScript example returns Base64-encoded screenshot data. Decode it to bytes and write to a unique path. This example uses Node’s built-in filesystem and path modules.

const fs = require('node:fs/promises');
const path = require('node:path');
const { Builder, Browser } = require('selenium-webdriver');

function safeComponent(value, fallback = 'unknown') {
  const cleaned = String(value || fallback)
    .replace(/[^A-Za-z0-9._-]+/g, '_')
    .replace(/^[._-]+|[._-]+$/g, '')
    .slice(0, 100);
  return cleaned || fallback;
}

(async () => {
  const runId = 'build-4812';
  const testId = 'checkout_guest_user';
  const workerId = '3';
  const attempt = '1';
  const dir = path.join('artifacts', 'screenshots', safeComponent(runId));
  await fs.mkdir(dir, { recursive: true });
  const file = path.join(dir, `${safeComponent(testId)}-worker-${safeComponent(workerId)}-attempt-${safeComponent(attempt)}.png`);
  const driver = await new Builder().forBrowser(Browser.CHROME).build();
  try {
    await driver.get('https://example.com');
    const base64 = await driver.takeScreenshot();
    await fs.writeFile(file, Buffer.from(base64, 'base64'), { flag: 'wx' });
    console.log(`Saved ${file}`);
  } finally {
    await driver.quit();
  }
})().catch(error => { console.error(error); process.exitCode = 1; });

The Node example uses exclusive-create mode (wx), which fails if that exact path already exists. This makes accidental replacement visible; resolve the identity collision or choose an intentional overwrite policy.

5. Saving directly versus handling screenshot data

Approach Good fit Check
Binding writes to a path Local browser process and CI workspace share a filesystem; simplest artifact collection Unique path, directory creation, return value or exception, CI upload path
Binding returns screenshot data or a temporary file You need to process, rename, store, or upload the image in test code Decode correctly, write atomically where appropriate, handle upload errors, retain execution identity in object keys

The available form depends on the binding: an API may save a file, return bytes, or return Base64 text. Selenium’s WebDriver screenshot endpoint returns Base64-encoded image data. Check the binding documentation for its exact return type and behavior.

6. Selenium Grid, containers, and CI artifact collection

  1. Identify the process doing the save. In a local setup, the test process and browser generally share the test environment. With Grid or a remote provider, determine whether the binding writes the returned screenshot on the client side or on a remote filesystem.
  2. Choose storage visible to that process. Use a workspace path available to the saver, or obtain screenshot data and write or upload it from a process that can reach the artifact store. Do not assume a path inside a browser container is automatically visible to the CI host.
  3. Keep destinations unique across machines too. If workers have separate disks but later upload to shared storage, use a unique object key with run, test, worker, and attempt identity. Separate local filesystems do not prevent a collision at a shared upload destination.
  4. Configure collection for the resulting directory or object prefix. Confirm that the CI artifact step runs after failures as well as successful tests, and that retry artifacts are retained according to your policy.
  5. Size Grid concurrency from the environment. Grid capacity depends on available processors and other resources. Treat documentation sizing examples as guidance and measure the target deployment rather than assuming a universal session limit. See Grid getting started guidance.

Grid distributes WebDriver execution; it does not choose screenshot filenames or define how your CI system stores artifacts. The exact path and transfer behavior depends on the language binding, remote endpoint, container mounts, and artifact arrangement.

7. Reliability, performance, and cost

  • Reliability: include all concurrent dimensions in the identity, especially shard and retry. Create directories before saving and fail visibly when saving or upload fails. Consider writing to a temporary unique path and renaming after a complete write when your storage supports atomic rename.
  • Performance: screenshots consume browser, process, disk, and upload resources. Capturing only on failure can reduce work if success screenshots are not needed. If every test needs evidence, keep capture work bounded and avoid serializing unrelated browser sessions just to protect filenames.
  • Storage: full-resolution PNG artifacts can accumulate quickly. Use retention and CI artifact limits appropriate to your project; avoid deleting the only evidence before an investigation is complete.
  • Cost: Selenium itself does not prescribe an artifact-storage price. Costs depend on your Grid or browser provider, CI execution, storage, retention, and network egress. Estimate from your own screenshot count, average file size, and retention period.

8. Troubleshooting

Symptom Likely cause Fix
Only one screenshot remains for many tests Workers share a fixed filename or upload key Add run, test, worker or shard, and attempt identity to the file path and remote object key.
The image belongs to a different test Shared destination was overwritten, or test identity was missing or reused Log the full destination alongside test and worker IDs; make the identity unique per execution.
Save fails with missing-directory error Parent directories were not created Create the directory recursively before calling the screenshot API.
Python returns False The screenshot file could not be written to the supplied path Check the path, permissions, parent directory, and whether the remote/local arrangement exposes that destination; treat the false result as a failed artifact.
Remote run succeeds but no CI artifact appears The file was saved somewhere CI does not collect, or the remote filesystem is isolated Verify which process materializes the file, then save on a mounted/shared path or transfer the image data to the artifact store.
Retries replace the original image Retry identity is absent from the filename Include attempt number or a unique execution ID and set a deliberate retention policy.
Upload still overwrites despite unique local files Object key or artifact name is shared Carry the same unique identity into the destination key; local uniqueness alone does not protect a shared store.
Filename contains invalid characters or unexpected folders Raw test names include separators or platform-sensitive characters Sanitize path components, cap length, and keep identity in metadata or logs if the readable name is shortened.

Or skip the browser setup

If you need a website image rather than a screenshot from the live Selenium test session, ScreenshotNeo provides a screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. This example saves a WebP response; see the ScreenshotNeo API documentation for parameters and response handling.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners are accepted like a visitor, and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
  • The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.

ScreenshotNeo is useful for repeatable page captures outside a particular Selenium session. It does not replace a Selenium screenshot when you need the exact state of an authenticated, interactive test browser. Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month with no card.

FAQ

Does Selenium Grid prevent screenshot filename collisions?

No. Grid routes WebDriver work to browser instances; your test code and artifact pipeline still need unique destinations.

Should I use a timestamp in every filename?

A timestamp can help, but use run, test, worker, and attempt identifiers as the primary identity. Those make files easier to associate with a test and avoid relying on clock precision.

Can two tests use the same test name safely?

Only if another component of the path distinguishes their executions, such as worker, shard, parameter ID, or attempt.

Should I save screenshots for passing tests?

That depends on whether you need visual records for successful runs. Capturing only on failure reduces artifact volume; capturing all runs can help diagnose intermittent behavior.