Capturing a website screenshot in C# typically means one of two paths: Selenium WebDriver (with ChromeDriver) or Puppeteer Sharp (a .NET port of Puppeteer). Both work. Both require you to ship a Chromium binary alongside your application, manage driver version matching, and handle browser process lifecycle in production.
This guide shows both approaches, explains where they break in production, and demonstrates how to replace them with a single HttpClient call to the SnapSharp screenshot API.
The Selenium WebDriver Approach
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
public static async Task<byte[]> ScreenshotWithSelenium(string url)
{
var options = new ChromeOptions();
options.AddArgument("--headless=new");
options.AddArgument("--no-sandbox");
options.AddArgument("--disable-dev-shm-usage");
options.AddArgument("--window-size=1280,720");
using var driver = new ChromeDriver(options);
try
{
driver.Navigate().GoToUrl(url);
await Task.Delay(2000); // Unreliable wait — replace with WebDriverWait in production
var screenshot = ((ITakesScreenshot)driver).GetScreenshot();
return screenshot.AsByteArray;
}
finally
{
driver.Quit(); // Must dispose or Chrome processes accumulate
}
}This requires the Selenium.WebDriver and Selenium.WebDriver.ChromeDriver NuGet packages, plus a matching Chrome installation.
What breaks in production
ChromeDriver version mismatch. Chrome updates automatically on most systems. ChromeDriver does not. After an OS update you will see SessionNotCreatedException: session not created: This version of ChromeDriver only supports Chrome version 114. The Selenium.WebDriver.ChromeDriver NuGet package pins to a specific Chrome version.
Azure App Service and containerized environments. Chrome requires a writable /dev/shm filesystem and specific kernel capabilities. In Azure App Service (Windows), Chrome does not run at all. In Linux containers you need --no-sandbox (a security trade-off) and a 2 GB+ memory allocation.
IDisposable management. Forgetting driver.Quit() leaks browser processes. In a web application under load, unclosed browser processes accumulate and exhaust server memory within hours.
Concurrent requests. Each new ChromeDriver() spawns a full browser process (~300–600 MB RAM). Building a thread-safe pool with ConcurrentQueue<IWebDriver> is not trivial and still requires capacity planning.
The Screenshot API Approach
The SnapSharp screenshot API handles the browser infrastructure on its side. Your C# code sends one HTTP request and receives a binary PNG or JPEG.
Using HttpClient (.NET 6+)
using System.Net.Http;
using System.Net.Http.Headers;
public class SnapSharpClient
{
private readonly HttpClient _httpClient;
private const string ApiBase = "https://api.snapsharp.dev/v1";
public SnapSharpClient(HttpClient httpClient)
{
_httpClient = httpClient;
var apiKey = Environment.GetEnvironmentVariable("SNAPSHARP_API_KEY")
?? throw new InvalidOperationException("SNAPSHARP_API_KEY is not set");
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
}
public async Task<byte[]> ScreenshotAsync(string url,
int width = 1280, int height = 720, string format = "png",
CancellationToken cancellationToken = default)
{
var encodedUrl = Uri.EscapeDataString(url);
var requestUrl = $"{ApiBase}/screenshot?url={encodedUrl}&width={width}&height={height}&format={format}";
var response = await _httpClient.GetAsync(requestUrl, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync(cancellationToken);
}
}Register as a singleton in Program.cs:
builder.Services.AddHttpClient<SnapSharpClient>();Usage in a controller or service:
public class ScreenshotController : ControllerBase
{
private readonly SnapSharpClient _snapSharp;
public ScreenshotController(SnapSharpClient snapSharp)
{
_snapSharp = snapSharp;
}
[HttpGet("screenshot")]
public async Task<IActionResult> Get([FromQuery] string url)
{
var bytes = await _snapSharp.ScreenshotAsync(url);
return File(bytes, "image/png", "screenshot.png");
}
}No using blocks to manage. No browser process lifecycle. No package updates when Chrome increments its version.
Full-page screenshots
var requestUrl = $"{ApiBase}/screenshot?url={encodedUrl}&full_page=true&width=1280&format=png";Requires Starter plan or higher.
Mobile device emulation
var device = Uri.EscapeDataString("iPhone 14");
var requestUrl = $"{ApiBase}/screenshot?url={encodedUrl}&device={device}&format=png";Available devices: iPhone 14, Pixel 7, iPad Pro, Galaxy S21, MacBook Pro 13.
Dark mode screenshots
var requestUrl = $"{ApiBase}/screenshot?url={encodedUrl}&dark_mode=true&width=1280&height=720";POST request with JSON body
For more complex parameters, the API also accepts POST with a JSON body:
var payload = new
{
url = "https://example.com",
width = 1280,
height = 720,
format = "png",
full_page = true,
block_ads = true,
delay = 1000
};
var content = new StringContent(
System.Text.Json.JsonSerializer.Serialize(payload),
System.Text.Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync($"{ApiBase}/screenshot", content, cancellationToken);
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);Error handling
public async Task<byte[]?> ScreenshotSafeAsync(string url)
{
try
{
var response = await _httpClient.GetAsync(
$"{ApiBase}/screenshot?url={Uri.EscapeDataString(url)}&width=1280&height=720");
if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
// Rate limit exceeded — back off and retry or return null
var retryAfter = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(60);
await Task.Delay(retryAfter);
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
catch (HttpRequestException ex)
{
// Log and handle — do not crash the request pipeline
Console.Error.WriteLine($"Screenshot API error: {ex.Message}");
return null;
}
}Saving to a file
var bytes = await client.ScreenshotAsync("https://github.com");
await File.WriteAllBytesAsync("screenshot.png", bytes);Comparison
| Selenium WebDriver | SnapSharp API | |
|---|---|---|
| Binaries required | Chrome + ChromeDriver | None |
| Azure App Service | Does not run on Windows tier | Works on all tiers |
| Docker image overhead | +300–500 MB | None |
| Concurrent requests | ~5–10 before OOM | Unlimited (rate-limited by plan) |
| Version maintenance | Chrome/driver sync required | None |
| Full-page capture | IJavaScriptExecutor scroll hack | full_page=true |
| Mobile emulation | ChromeOptions per device | device=iPhone+14 |
| Caching | DIY | Built-in Redis, cache hits free |
| Cold start | 3–8s (Chrome launch) | 0ms (warm pool) |
NuGet
No additional packages are required. System.Net.Http.HttpClient is built into .NET 6+.
If you are on .NET Framework 4.x, use HttpWebRequest or install System.Net.Http from NuGet.
Getting started
- Create a free account — no credit card required
- Go to Dashboard → API Keys → Create key
- Set the environment variable:
SNAPSHARP_API_KEY=sk_... - Use the
SnapSharpClientclass above in your ASP.NET Core or .NET console project
The free tier includes 100 screenshots/month. The Starter plan provides 5,000/month with full-page capture, retina resolution, and device emulation.