Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To capture many URLs with Playwright for .NET, launch a browser once, create a context that matches the session and viewport you need, then navigate to each URL and call Page.ScreenshotAsync. Save each capture to a unique path, record failures per URL, and tune concurrency against your own machine and target sites rather than assuming a universal worker count. Use FullPage = true for the full scrollable page, or a locator screenshot for one element. [Playwright screenshots guide]

Choose the capture and session model first

Bulk screenshot code gets easier to reason about when three decisions are made up front: what part of each page to capture, which pages should share browser state, and whether the work should run serially or concurrently.

  • Capture scope: use the viewport screenshot for the currently visible area, FullPage = true for the full scrollable page, or Locator.ScreenshotAsync when only a particular element matters. [Screenshots guide] [Locator API]
  • Output: give ScreenshotAsync a path to save directly, or omit the path and use its returned bytes for processing or storage. [Page API]
  • State: pages in one browser context share that context’s session; use separate contexts when captures need independent cookies, local storage, or session state. A context can contain multiple pages. [Browser contexts] [Pages]

A new browser process for every URL is usually unnecessary. Reuse a browser for the batch and decide whether to reuse a page, create more pages within a context, or isolate work in separate contexts according to the state the capture requires. Browser contexts are lightweight and can be closed after their work is complete. [Browser contexts]

Set up Playwright for .NET

Install the Playwright .NET package in your project, then install the browser binaries for the engine you intend to run. Playwright .NET supports Chromium, Firefox, and WebKit, locally and in CI. Choose the engine or engines that reflect the purpose of the captures; there is no need to run every engine if the output only needs to represent one browser. Consult the official [installation guide] for the current package and browser installation steps for your project type.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The example below is a console-style batch utility. It uses Chromium, one context, one page at a time, and a bounded overall timeout for navigation. It writes each successful screenshot to a sanitized, indexed filename and continues after an individual URL fails. Add the Playwright .NET package and install its Chromium browser before running it.

Run a simple, reliable batch

using Microsoft.Playwright;
using System.Text;

var urls = new[]
{
    "https://example.com/",
    "https://playwright.dev/dotnet/docs/screenshots",
    "https://playwright.dev/dotnet/docs/browser-contexts"
};

Directory.CreateDirectory("screenshots");

using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new()
{
    Headless = true
});

await using var context = await browser.NewContextAsync(new()
{
    ViewportSize = new() { Width = 1440, Height = 900 }
});

var page = await context.NewPageAsync();
var failures = new List<(string Url, string Error)>();

for (var i = 0; i < urls.Length; i++)
{
    var url = urls[i];
    var fileName = $"{i + 1:D4}-{SafeName(url)}.png";
    var outputPath = Path.Combine("screenshots", fileName);

    try
    {
        await page.GotoAsync(url, new()
        {
            WaitUntil = WaitUntilState.Load,
            Timeout = 45_000
        });

        await page.ScreenshotAsync(new()
        {
            Path = outputPath,
            FullPage = true
        });

        Console.WriteLine($"Saved {url} to {outputPath}");
    }
    catch (Exception ex)
    {
        failures.Add((url, ex.Message));
        Console.Error.WriteLine($"Failed {url}: {ex.Message}");
    }
}

await browser.CloseAsync();

if (failures.Count > 0)
{
    Console.Error.WriteLine($"{failures.Count} of {urls.Length} captures failed:");
    foreach (var failure in failures)
        Console.Error.WriteLine($"- {failure.Url}: {failure.Error}");
}

static string SafeName(string value)
{
    var invalid = Path.GetInvalidFileNameChars().ToHashSet();
    var cleaned = new string(value.Select(c => invalid.Contains(c) ? '_' : c).ToArray());
    return cleaned.Length > 80 ? cleaned[..80] : cleaned;
}

The indexed prefix ensures that two URLs with the same sanitized name do not overwrite each other in this run. For recurring jobs, consider a stable identifier from your input data or a hash of the full URL, and persist a manifest mapping output files to their original URLs. Treat the manifest and naming scheme as part of the batch output, not as a guarantee provided by Playwright.

WaitUntilState.Load waits for the page’s load event, but that does not guarantee that every application has finished rendering dynamic content. If a capture depends on a specific component, wait for its locator or another known readiness condition before taking the screenshot. Choose that condition from the site you are capturing; a fixed delay is not a reliable substitute for application-specific readiness.

Capture a viewport, full page, or element

The sample uses FullPage = true. Omit that option to capture the viewport. If you only need a particular element, use the locator API instead of saving a full-page image:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
var card = page.Locator(".product-card");
await card.ScreenshotAsync(new()
{
    Path = "screenshots/product-card.png"
});

Locator screenshots are useful for repeatable component captures, but the selector must identify the intended element on each page. If the locator does not resolve or the target is hidden, the capture will fail; handle those cases as item-level errors in a batch. [Locator API]

Keep or isolate browser state

The example uses one context and one page because its URLs are treated as a serial sequence in the same session. That is appropriate when the intended workflow shares session state, such as visiting several pages after a sign-in. When URLs should not inherit cookies or local storage from earlier work, create a separate context for each independent session or group of captures, and close it when that work finishes. Do not create a fresh browser process for every URL unless your operational requirements actually call for process isolation. [Browser contexts]

Handle output and failures deliberately

For a batch, one inaccessible URL should not erase the useful results from every other URL. The sample catches each item’s exception, reports the URL and error, then carries on. At the end, it reports the number of failures. For a production job, also write structured records containing the input identifier, URL, output path, status, and error so the batch can be audited or retried selectively.

When you need to transform images, upload them to another service, or compute metadata, use the screenshot bytes rather than writing and rereading a temporary file. The page screenshot API can return image bytes when no path is supplied. Conversely, writing directly to a path is simpler when the desired result is a directory of files. [Page API]

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use deterministic but collision-resistant names. A filename derived only from a URL’s final path segment can collide across domains or query variants. Include an index or input key, and preserve the original URL separately because filesystem-safe names are not necessarily reversible.

Scale the batch without guessing at concurrency

A serial loop is the simplest baseline: it limits simultaneous page activity and makes failures easy to associate with a URL. If it is too slow, introduce bounded concurrency and increase it gradually while observing memory, CPU, network, and the behavior of the destination sites. More concurrent pages can increase resource use and can cause rate limits or unstable captures. The official Playwright .NET documentation describes parallel execution through NUnit, MSTest, xUnit, and xUnit v3, but it does not establish a universal worker count for arbitrary screenshot jobs. [Writing tests] [Running tests]

For parallel captures, use a bounded worker pool rather than starting one task per URL without a limit. Decide whether workers share a context or receive separate contexts based on the session boundary; separate contexts prevent jobs from sharing cookies and other state, while a shared context may be appropriate where common session state is intentional. Keep output paths unique across workers. Tune the limit with the actual page mix and execution environment; neither a safe maximum nor expected throughput is established for all workloads.

When screenshots are part of automated tests rather than a custom data-collection job, Playwright’s .NET integrations for NUnit, MSTest, xUnit, and xUnit v3 provide test-runner parallelism configuration. Use the runner that fits the existing test suite and configure workers there, instead of adding a separate concurrency mechanism without a reason. [Writing tests] [Running tests]

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshoot common bulk-capture problems

  • Browser executable is missing: the Playwright package alone is not the browser binary. Complete the browser installation step for the engine you selected, following the current .NET installation instructions.
  • Navigation times out: the site may be slow, unreachable, or still performing work after the chosen readiness event. Check the URL and connectivity, set a timeout appropriate for the workload, and wait for a specific application element if the capture requires it. Record the failed item and continue or retry it according to your job policy.
  • The screenshot is blank or incomplete: the page may not have rendered the content you expect when capture begins. Replace a generic load assumption with a locator or other page-specific readiness condition; confirm that the requested viewport or full-page scope matches the desired output.
  • An element screenshot fails: verify that the selector matches the intended element and that it is present and visible on that page. A selector that works on one URL may not exist on another, so handle it per item.
  • Files overwrite one another: different input URLs may normalize to the same filename. Add an input index or unique key and keep a manifest containing the original URL and output path.
  • The process becomes unstable as concurrency rises: reduce the number of simultaneous pages and measure CPU, memory, and network behavior before raising it again. There is no source-backed universal concurrency limit for general-purpose bulk screenshots.

Or skip the browser setup

If you want a hosted screenshot API rather than managing Playwright and browser binaries, ScreenshotNeo accepts one GET request with a URL and returns a PNG, JPEG, WebP, or PDF. The example below requests a screenshot of one URL; for bulk work, call it for each input URL while applying your own bounded concurrency and output naming.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for request options. Its clean-shot flow accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can each be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether it was billed. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account to try 1,000 screenshots per month with no card.

Which approach fits your job?

Need Suitable approach
Capture a controlled set of pages within a local .NET utility Use Playwright with a reused browser, an intentional context boundary, and item-level error handling.
Capture only an element or full scrollable document Use locator-based screenshot capture for an element, or FullPage = true for a full-page capture.
Cross-browser coverage matters Run against the required Chromium, Firefox, and/or WebKit engines supported by Playwright .NET. [Installation]
You want to avoid installing and operating browser binaries Consider a hosted API such as ScreenshotNeo; keep batch concurrency and filenames under your application’s control.
Captures are assertions or artifacts in an existing .NET test suite Use a supported Playwright test-runner integration and its parallel execution configuration.

Frequently Asked Questions

Can one Playwright browser context contain multiple pages?

Yes. A browser context can contain multiple pages; use the same context when those pages should share session state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does Playwright recommend a fixed number of concurrent screenshot workers?

The cited .NET documentation does not specify a universal worker count for arbitrary bulk screenshot jobs. Tune a bounded limit against your workload and environment.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.