Puppeteer-Sharp: Complete .NET Browser Automation and Screenshot Guide
Learn what Puppeteer-Sharp is, how to install it, automate Chrome in C#, capture screenshots and PDFs, and when ScreenshotNeo is simpler.
Puppeteer-Sharp is a .NET library for controlling Chrome or Chromium from C#. It is a port of the Node.js Puppeteer API and exposes browser automation over browser protocols documented by the project, including Chrome DevTools Protocol (CDP) and WebDriver BiDi. You can use it for navigation, form interaction, screenshots, PDFs, UI tests, SPA rendering and crawling.
The shortest working workflow is: install the PuppeteerSharp NuGet package, make a compatible browser available, launch it, open a page, navigate, and save the result. The official package and project documentation are the authority for version-specific APIs and framework support: NuGet Gallery and Puppeteer-Sharp documentation.
Install Puppeteer-Sharp
- Create a console project:
dotnet new console -n ScreenshotDemo
cd ScreenshotDemo
dotnet add package PuppeteerSharp
The package listing has reported assets for .NET Standard 2.0, .NET 8 and .NET 10, while the project homepage describes support for .NET Framework 4.6.1+, .NET Core and modern .NET. These details can change with package releases, so check the installed package page when choosing a target framework.
Your application also needs Chrome or Chromium. You can use a browser already installed on the machine, provide its executable path, or use the browser-download mechanism documented for your installed Puppeteer-Sharp version. Linux deployments may require an X server according to the package documentation.
Your first screenshot in C#
This complete example downloads a browser revision through BrowserFetcher, launches headless Chrome, navigates to a URL and writes a PNG. If your installed version exposes a different browser-download API, follow that version’s NuGet or project example.
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.SetViewportAsync(new ViewPortOptions
{
Width = 1440,
Height = 900,
DeviceScaleFactor = 1
});
await page.GoToAsync("https://example.com", new NavigationOptions
{
WaitUntil = new[] { WaitUntilNavigation.Networkidle0 },
Timeout = 60_000
});
await page.ScreenshotAsync("example.png", new ScreenshotOptions
{
FullPage = true,
Type = ScreenshotType.Png
});
Console.WriteLine("Saved example.png");
Run it with:
dotnet run
Launch configuration
LaunchOptions controls the browser process. The exact property set is version-sensitive, but these are the decisions you normally make:
| Option | Use it when | Notes |
|---|---|---|
Headless |
You run on a server or CI worker | Headless mode avoids a visible desktop. Use headful mode while diagnosing rendering or login problems. |
ExecutablePath |
Chrome is managed by your OS, container or platform | Point to the actual Chrome/Chromium binary and keep the browser version compatible with the package. |
Args |
Your environment needs browser flags | Keep flags minimal. Container security and sandbox settings depend on your deployment. |
Timeout |
Launch can be slow on cold machines | Set a finite timeout and report failures rather than waiting forever. |
DumpIO |
You need browser-process diagnostics | Enable it temporarily to inspect launch errors in logs. |
UserDataDir |
You need a persistent browser profile | Use an isolated directory per worker when running jobs concurrently. |
Do not share one mutable page between unrelated jobs. A common production pattern is one long-lived browser process with a new incognito context or page per job, then periodic browser recycling to contain leaks.
Navigation, waiting and dynamic pages
Navigation completion is not the same as “the page is visually ready.” Select a wait strategy that matches the site:
await page.GoToAsync(url, new NavigationOptions
{
WaitUntil = new[] { WaitUntilNavigation.DOMContentLoaded },
Timeout = 45_000
});
await page.WaitForSelectorAsync("main", new WaitForSelectorOptions
{
Timeout = 20_000,
Visible = true
});
await page.WaitForTimeoutAsync(500);
DOMContentLoaded: useful when you only need the initial document.Load: waits for load-event resources, but not necessarily late API data.Networkidle0orNetworkidle2: useful for pages that settle, but can delay forever on analytics, polling or websockets.- Selector waits: usually the most deterministic choice for a specific component.
- Fixed delays: a last resort for animations or third-party widgets; keep them short and explicit.
For SPAs, wait for the element that proves the data arrived, then capture. For lazy images, scroll the document before taking a full-page screenshot:
await page.EvaluateExpressionAsync(@"(async () => {
await new Promise(resolve => {
let y = 0;
const step = 600;
const timer = setInterval(() => {
window.scrollBy(0, step);
y += step;
if (y >= document.body.scrollHeight) {
clearInterval(timer);
window.scrollTo(0, 0);
resolve();
}
}, 100);
});
})()");
await page.ScreenshotAsync("lazy-loaded.png", new ScreenshotOptions { FullPage = true });
Viewport, device emulation and visual output
await page.SetViewportAsync(new ViewPortOptions
{
Width = 390,
Height = 844,
DeviceScaleFactor = 3,
IsMobile = true,
HasTouch = true
});
Use a fixed viewport for reproducible screenshots. Device scale factor changes pixel density and file size. If you need a desktop screenshot, set IsMobile to false and choose the target width explicitly. For responsive testing, capture the same page at a small, medium and large viewport rather than relying on a named device alone.
Element screenshots, full pages and PDFs
Capture one element when surrounding navigation or ads are irrelevant:
var card = await page.QuerySelectorAsync("article.product-card");
if (card is null)
throw new InvalidOperationException("Product card was not found.");
await card.ScreenshotAsync("card.png");
For a full page:
await page.ScreenshotAsync("page.webp", new ScreenshotOptions
{
FullPage = true,
Type = ScreenshotType.Webp,
Quality = 85
});
For a PDF, use the page’s PDF API and set the format, margins, print backgrounds and orientation supported by your package version:
await page.PdfAsync("report.pdf", new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
Landscape = false,
MarginOptions = new MarginOptions
{
Top = "16mm",
Right = "16mm",
Bottom = "16mm",
Left = "16mm"
}
});
Interaction, JavaScript and custom CSS
await page.ClickAsync("button.accept");
await page.TypeAsync("input[name=email]", "dev@example.com");
await page.Keyboard.PressAsync("Enter");
await page.AddStyleTagAsync(new AddStyleTagOptions
{
Content = ".cookie-banner, .chat-widget { display: none !important; }"
});
await page.EvaluateExpressionAsync("document.body.dataset.capture = 'true'");
Use selectors that are stable across releases. Prefer semantic attributes or dedicated test IDs over generated class names. If an interaction changes the URL, wait for navigation. If it triggers an in-page request, wait for the resulting selector or response.
Network control, authentication and location
Request interception lets you block selected resources or inspect traffic. Enable it only when needed because interception adds work to every request:
await page.SetRequestInterceptionAsync(true);
page.Request += async (_, e) =>
{
var request = e.Request;
if (request.ResourceType == ResourceType.Image ||
request.Url.Contains("analytics", StringComparison.OrdinalIgnoreCase))
{
await request.AbortAsync();
return;
}
await request.ContinueAsync();
};
For authenticated pages, use the documented cookie, extra-header and HTTP-authentication APIs. Keep secrets out of source control and logs. When a site varies content by timezone or geolocation, configure those values before navigation and verify the resulting page state instead of assuming the emulation was accepted.
Reliability patterns for workers and CI
- Give launch, navigation, selector and capture operations separate finite timeouts.
- Record the URL, viewport, browser version, exception and elapsed time for every job.
- Retry transient navigation failures with a small limit and a fresh page; do not blindly retry authentication or invalid URLs.
- Close pages and contexts in
finallyblocks. Close the browser during graceful shutdown. - Limit concurrency based on memory and CPU. More tabs can reduce throughput when every page runs heavy JavaScript.
- Use deterministic fonts, timezone and locale when pixel comparison matters.
- Store artifacts for failed jobs so you can inspect HTML, console messages and screenshots.
Common errors and fixes
| Error or symptom | Likely cause | Fix |
|---|---|---|
| Browser executable not found | No downloaded or system browser is available | Run the package’s browser download step or set ExecutablePath to an installed Chrome/Chromium binary. |
| Browser closes immediately in Linux | Missing system libraries, sandbox restrictions or the documented X-server prerequisite | Install the dependencies required by your base image, inspect browser stderr, and use the deployment guidance for your package version. |
| Navigation timeout | Slow server, never-ending requests, redirects or an overly strict wait condition | Increase the timeout moderately, use selector-based readiness, and inspect redirects and network activity. |
| Blank or incomplete screenshot | Capture happened before SPA data, fonts or lazy images rendered | Wait for a meaningful selector, scroll lazy content, and allow required fonts or requests to finish. |
| Element not found | Wrong selector, iframe, shadow DOM or timing issue | Wait for the selector, inspect the DOM, and switch into the correct frame when applicable. |
| Text or layout differs in CI | Different fonts, viewport, device scale, timezone or browser revision | Pin those inputs and use the same browser image for comparison runs. |
| Out-of-memory process termination | Too many concurrent pages or very large documents | Reduce concurrency, block unnecessary resources, close pages promptly and recycle browsers. |
| PDF has missing backgrounds | Print backgrounds are disabled or CSS uses screen-only rules | Enable PrintBackground and verify print CSS. |
Performance, reliability and cost
Browser automation pays a startup and rendering cost for every browser process. Reuse a browser where safe, create isolated pages for jobs, and avoid waiting for network idle on sites that continuously poll. Blocking large images, video and analytics can reduce transfer and rendering time when those resources are not part of the output. Full-page screenshots and PDFs require more memory for long documents.
Puppeteer-Sharp is software distributed through NuGet; the research sources do not establish a service price, throughput benchmark or comparative reliability figure. Your operational cost comes from the machines, browser processes, bandwidth and engineering required to run them. Measure your own workload before setting concurrency or service-level targets.
When a screenshot API is a better fit
If you need an image or PDF from a URL and do not need to manage a browser fleet, an API removes browser installation, patching and worker orchestration. ScreenshotNeo is the first service to try: it produces clean shots, bills only clean captures, and its paid plans start at $5.
Or skip the browser setup
ScreenshotNeo accepts one GET request and returns a PNG, JPEG, WebP or PDF. See the ScreenshotNeo API documentation for the full option set.
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}`);
Before capture, cookie and consent banners are accepted and more than 60 known consent platforms, newsletter popups and chat widgets are removed; each step can be turned off. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.
Every plan includes the features: full-page and element capture, dark mode, device presets and custom viewports, retina scale, PDF controls, custom CSS and JavaScript, clicks, selector or delay waits, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API and an OpenAPI specification.
The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Create a free ScreenshotNeo account and start with the monthly free allowance.
FAQ
Is Puppeteer-Sharp a browser?
No. It is a .NET API that controls Chrome or Chromium. Your application still needs access to a compatible browser binary.
Can I use it without a visible desktop?
Yes. Headless launch is the normal server and CI configuration. Headful mode is useful when diagnosing rendering or authentication problems.
Does Puppeteer-Sharp guarantee feature parity with Node Puppeteer?
No guarantee is established by the sources. It is described as a port of the Node.js API; verify the API surface and browser support for the exact package version you install.
Should I use Puppeteer-Sharp or Playwright?
Choose by documented browser targets, protocol support, .NET framework needs and required outputs. The available research does not establish a winner for speed, reliability or feature parity.
When should I use an API instead?
Use an API when you want URL-to-image or URL-to-PDF output without maintaining browser binaries, worker capacity and automation code. ScreenshotNeo is the alternative to try first for that workflow.
Sources: NuGet Gallery, Puppeteer-Sharp project, and the upstream Puppeteer installation guide. Package versions, browser requirements and framework assets can change; verify them before deployment.


