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

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To use Puppeteer-style browser automation in C#, install PuppeteerSharp, download its compatible browser, launch it asynchronously, create a page, and then navigate, interact, capture, or generate a PDF. PuppeteerSharp is described by its project documentation as “a .NET port of the official Node.JS Puppeteer API.”

dotnet add package PuppeteerSharp

The complete workflow is asynchronous, and every browser and page should be disposed when the operation finishes.

Install PuppeteerSharp and a compatible browser

Create or open a .NET application, then add the NuGet package:

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

The package version and supported target frameworks change over time. Check the exact NuGet version you select and the project README before committing to a target framework. The NuGet listing returned version 25.12.0 on the date of the referenced research, but that number is volatile.

PuppeteerSharp’s README recommends its bundled Chromium for guaranteed compatibility. Using a different browser executable is possible, but the project describes that choice as being at the user’s risk. The README also lists an X server requirement for Linux; verify the current deployment prerequisites for your distribution and hosting environment.

Download the browser at startup

BrowserFetcher downloads the browser revision expected by the installed package:

using PuppeteerSharp;

var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();

In production, download the browser during image creation or deployment rather than on every request. Ensure the process can write to the browser cache directory and that the deployed account has permission to execute the browser.

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

Minimal C# example: navigate and take a screenshot

This is the smallest useful end-to-end program:

using PuppeteerSharp;

var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();

await using var browser = await Puppeteer.LaunchAsync(
    new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();

await page.GoToAsync("https://example.com");
await page.ScreenshotAsync("screenshot.png");

LaunchAsync, NewPageAsync, navigation, and the capture are all asynchronous. The await using declarations make disposal visible: the page is closed first and the browser is then shut down even if a later operation throws.

Control the viewport

Set the viewport before navigation when layout, responsive breakpoints, or screenshot dimensions matter:

await page.SetViewportAsync(new ViewPortOptions
{
    Width = 1440,
    Height = 900,
    DeviceScaleFactor = 1
});

await page.GoToAsync("https://example.com");
await page.ScreenshotAsync("desktop.png", new ScreenshotOptions
{
    FullPage = true
});

A fixed viewport makes runs more reproducible. FullPage = true captures the complete document rather than only the visible viewport; pages that lazy-load content may need an additional scroll or readiness wait before capture.

Navigate reliably

Navigation can finish before an application has rendered its useful state. Choose a readiness condition that matches the site rather than assuming that the first response means the page is ready.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.GoToAsync("https://example.com/dashboard", new NavigationOptions
{
    WaitUntil = new[] { WaitUntilNavigation.Networkidle0 },
    Timeout = 60_000
});

Single-page applications may continue making requests indefinitely, so waiting for a specific selector or application condition is often more reliable than waiting for network idle alone.

Select and interact with elements

Use locators for ordinary actions

PuppeteerSharp documents locators with built-in auto-retry and auto-wait. Use them for buttons, links, and form controls:

var email = page.Locator("input[name='email']");
await email.FillAsync("dev@example.com");

await page.Locator("button.submit").ClickAsync();

A locator is preferable to immediately querying a node when the element may appear after client-side rendering. Use stable attributes such as a data-test identifier when you control the page; long positional CSS selectors are more likely to break after a redesign.

Wait for a selector

When the next step depends on a particular element, wait for that element explicitly:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.WaitForSelectorAsync(".results");
var text = await page.Locator(".results").InnerTextAsync();

Set an appropriate timeout for the operation and handle timeout exceptions so a slow or changed page produces a useful diagnostic instead of an unexplained failure.

Wait for a browser-side condition

For state that cannot be represented by one selector, use a page function:

await page.WaitForFunctionAsync(
    "() => document.querySelectorAll('.result').length > 0");

This executes the predicate in the page context. Keep the predicate inexpensive and narrowly scoped.

Run JavaScript in the page

Use EvaluateExpressionAsync<T> for an expression or EvaluateFunctionAsync<T> for a function. The return value is deserialized into the requested C# type:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
var title = await page.EvaluateExpressionAsync<string>(
    "document.title");

var links = await page.EvaluateFunctionAsync<string[]>(@"
    () => Array.from(document.querySelectorAll('a'))
              .map(a => a.href)");

foreach (var link in links)
{
    Console.WriteLine(link);
}

Browser-side code can read the rendered DOM, but it cannot directly access C# variables unless you pass values as function arguments. Be careful with pages that use frames: an element inside an iframe belongs to that frame’s document and must be queried through the appropriate frame.

Connect to an existing or remote browser

Launching a local browser is not the only deployment model. PuppeteerSharp exposes Puppeteer.ConnectAsync with ConnectOptions for a browser that provides a WebSocket endpoint:

using PuppeteerSharp;

var browser = await Puppeteer.ConnectAsync(new ConnectOptions
{
    BrowserWSEndpoint = Environment.GetEnvironmentVariable("BROWSER_WS")
        ?? throw new InvalidOperationException("BROWSER_WS is missing")
});

await using (browser)
await using (var page = await browser.NewPageAsync())
{
    await page.GoToAsync("https://example.com");
    Console.WriteLine(await page.TitleAsync());
}

Keep the endpoint secret and restrict network access to it. The project site advertises both Chrome DevTools Protocol (CDP) and WebDriver BiDi support; protocol details can change between releases, so consult the version-specific documentation when relying on protocol behavior.

Take screenshots beyond the basics

Capture an element

To capture only a component, locate it and use the element’s screenshot operation:

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

If the element is hidden, outside the rendered layout, or covered by another layer, make it visible first and wait for the final state.

Allow fonts and lazy content to settle

For a page that loads assets after navigation, wait for the relevant selector, a page function, or a short, justified delay before capturing. A full-page capture does not guarantee that an image loaded only after scrolling has already been fetched; trigger the page’s lazy-loading behavior when necessary.

Generate PDFs

PuppeteerSharp supports PDF generation through PdfAsync. Chrome headless is currently required for this operation:

await page.GoToAsync("https://example.com/invoice");
await page.PdfAsync("invoice.pdf", new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true,
    Landscape = false,
    MarginOptions = new MarginOptions
    {
        Top = "16mm",
        Right = "16mm",
        Bottom = "16mm",
        Left = "16mm"
    }
});

The API reference distinguishes generating a PDF from a page from navigating to a PDF document: headless mode does not support navigating to a PDF document, while rendering the current HTML page to PDF is supported.

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

Wait for web fonts

If fonts are loaded from a CDN, the project README demonstrates waiting for document.fonts.ready before calling PdfAsync. Without that wait, the PDF may contain no rendered text:

await page.GoToAsync("https://example.com/report");
await page.EvaluateExpressionAsync("document.fonts.ready");
await page.PdfAsync("report.pdf", new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true
});

Common failures and fixes

Browser executable not found

Cause: the compatible browser was never downloaded, the cache is unavailable, or the process lacks permission to execute it.

Fix: run BrowserFetcher.DownloadAsync() during deployment, preserve the cache in the runtime image, and verify executable permissions. If you provide an executable path manually, make sure it matches the PuppeteerSharp package’s supported browser expectations.

Navigation timeout

Cause: a slow server, a page that never becomes idle, a blocked network request, or an overly short timeout.

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

Fix: increase the timeout for the known workload, use a selector-based readiness check, and log the URL and the last operation. Do not hide persistent failures by setting an unlimited timeout.

Element cannot be clicked or filled

Cause: the selector matches nothing, the element is still rendering, it is covered by a modal, or it is inside an iframe.

Fix: wait for the selector, inspect the rendered DOM, dismiss the overlay, and query the correct frame. Prefer a locator with a stable selector.

Blank or incomplete screenshot

Cause: capture occurred before fonts, images, or client-side data finished loading.

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

Fix: wait for a meaningful selector or function, wait for document.fonts.ready when typography matters, and exercise lazy-loading paths before a full-page capture.

PDF has missing text

Cause: web fonts were not ready when Chrome printed the page.

Fix: evaluate document.fonts.ready before PdfAsync. Also confirm that the operation is PDF generation from an HTML page, not navigation to a PDF URL.

Linux launch fails

Cause: missing system libraries, sandbox restrictions, or the X-server requirement documented by the project for Linux.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Fix: check the current PuppeteerSharp README for your distribution, install the listed dependencies, and validate the same container or VM image used in production.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and cost considerations

  • Reuse deliberately: browser startup is expensive. A long-lived browser with isolated pages can reduce startup overhead, but close pages and limit concurrency to avoid memory growth.
  • Bound every wait: navigation, selectors, functions, and external work should have timeouts. Record URL, selector, and exception details for diagnosis.
  • Keep browser and package aligned: the bundled Chromium is the project’s guaranteed pairing; changing either side can introduce rendering or protocol differences.
  • Separate jobs: use a fresh page or browser context for unrelated users and clear authentication state when data isolation matters.
  • Make output deterministic: fix viewport, timezone, locale, fonts, and network-dependent data where reproducible screenshots or PDFs are required.

Or skip the browser setup

If your goal is simply a clean screenshot or PDF rather than maintaining Chromium in your C# service, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF.

It removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server includes take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

cURL

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

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());

See the complete parameter list and response behavior in the ScreenshotNeo documentation. Options include full-page and element capture, dark mode, device presets, retina scale, PDF paper and page ranges, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification.

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 Free plan includes 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 screenshots; every feature is included on every plan. Create a free ScreenshotNeo account to try it.

Frequently Asked Questions

Is PuppeteerSharp the official Puppeteer package for .NET?

No. PuppeteerSharp is a community .NET port of the official Node.js Puppeteer API; install it as the PuppeteerSharp NuGet package.

Can PuppeteerSharp automate an already running browser?

Yes. Use Puppeteer.ConnectAsync with ConnectOptions and a browser WebSocket endpoint instead of LaunchAsync.

Can headless PuppeteerSharp open a PDF URL?

The API notes that headless mode does not support navigating to a PDF document. Generate a PDF from an HTML page with PdfAsync instead.

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

Why does a PDF sometimes contain no text?

Web fonts may not have finished loading. Evaluate document.fonts.ready before calling PdfAsync.

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.