The journal · 19 July 2026 · 9 min read
HTML to PDF in C#: the libraries that actually work in 2026
Half the C# HTML to PDF advice still points at a wrapper around an archived binary. Here is the current map for .NET: which library renders modern CSS, which is a security liability, and the code for each.
Converting HTML to PDF in C# looks solved until you actually ship it. The top search results still recommend a wrapper around wkhtmltopdf, an engine that was archived in 2023 and carries an unpatched security flaw, and half the sample code renders a page before its JavaScript has run. This post maps the real options for .NET in 2026: which library renders modern CSS correctly, which one is a licensing or security liability, the working code for each, and the point where handing the render to an API is cheaper than owning it.
The short version, if you want it now: for pixel accurate output that matches Chrome, drive a real browser with PuppeteerSharp or use a rendering API. For simple, static, print oriented documents you fully control, a pure .NET engine is fine. Do not build anything new on wkhtmltopdf.
The four families of C# HTML to PDF tools
Every option in .NET falls into one of four buckets, and they fail in different ways.
Real browser engines: PuppeteerSharp and Playwright for .NET
PuppeteerSharp is a faithful .NET port of Puppeteer that downloads and drives a headless Chromium build, then calls its print-to-PDF function. Playwright also ships a first class .NET package that does the same across Chromium, WebKit and Firefox. This is the gold standard for fidelity: real CSS grid and flexbox, real web fonts, real JavaScript execution. If the page looks right in Chrome, the PDF looks right too.
// PuppeteerSharp: render a live URL to PDF
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(
new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();
await page.GoToAsync("https://app.example.com/invoices/8412",
WaitUntilNavigation.Networkidle0);
await page.WaitForSelectorAsync("#invoice-total");
await page.PdfAsync("invoice.pdf", new PdfOptions {
Format = PaperFormat.A4, PrintBackground = true });
Two details do most of the work here. Networkidle0 waits until the network is quiet, and WaitForSelectorAsync holds the render until the element you actually care about, the invoice total, is on the page. Skip them and you will render a spinner. PrintBackground = true is the other one everyone forgets, because Chromium drops background colors and images in print mode by default.
The cost of this fidelity is operational. You are now running Chromium as part of your service. A single instance idles at 100 to 300 MB and spikes higher on heavy pages, the processes leak and zombie under load, and a serious deployment ends up with a restart-after-N-renders policy and a queue to cap concurrency. On Windows and in containers you also inherit Chromium's system dependency list. None of this is hard once; all of it is a recurring tax.
The wkhtmltopdf wrappers: DinkToPdf, WkHtmlToPdf-DotNet, Rotativa
DinkToPdf, WkHtmlToPdf-DotNet and Rotativa all wrap the wkhtmltopdf binary. They are popular because they are cheap and simple: no browser download, low memory, one native library. The problem is the engine underneath. wkhtmltopdf uses an ancient Qt WebKit build whose feature set froze years ago, so no CSS grid, unreliable flexbox, and page-break bugs that slice table rows in half. Its maintainers archived the project in 2023, and it carries CVE-2022-35583, a high severity SSRF issue that will never be patched. We lay out the full picture and the per stack replacements in the wkhtmltopdf alternative breakdown.
DinkToPdf adds its own wrinkle in modern .NET: it depends on a native library you have to ship and load correctly per platform, and it is not thread safe, so you serialize conversions behind its synchronized dispatcher. If you have an existing app on DinkToPdf that renders simple static invoices and never touches modern CSS, it will keep working. Do not start a new project on it, and schedule the migration on the old one, because the security exposure alone is reason enough.
Pure .NET document engines: PdfSharp, QuestPDF, iText
These do not render HTML as a browser does; they build a PDF from a document model in code. PdfSharp and its MigraDoc layer draw pages programmatically. QuestPDF is the standout modern option, a genuinely pleasant fluent C# API for laying out documents, with a free community license for smaller companies and a paid tier above a revenue threshold. iText (the successor to iTextSharp) is powerful and battle tested but carries an AGPL license that requires a commercial purchase for closed source use, which catches teams by surprise.
The honest tradeoff: if your document is defined in code rather than as an existing HTML template, these engines produce crisp, small, print correct PDFs with real pagination and no browser to operate. If your source of truth is an HTML template that your designers own, rebuilding it as a QuestPDF layout is a rewrite, and every future design change has to happen twice. Some libraries advertise HTML to PDF conversion on top of a document engine, but that HTML support is a limited subset, not a browser, so complex CSS and any JavaScript will not survive.
Commercial all-in-one: IronPDF and friends
IronPDF is the most marketed C# option and it is a capable product: it embeds a Chromium renderer, so fidelity is good, and the API is clean. It is commercial software with per developer and per deployment licensing that starts in the hundreds of dollars and climbs with team size and redistribution. For a team that wants one supported NuGet package, does not want to operate a browser, and is fine paying for a license, it is a reasonable buy. Just price the license against your actual seat and deployment count, because that is where the number moves. One caveat if you are rendering from Blazor: IronPDF and the other server-side engines cannot run inside a Blazor WebAssembly app, so the Blazor HTML to PDF page covers what changes between the Server and WebAssembly hosting models.
C# HTML to PDF options at a glance
| Option | Engine | JS and modern CSS | Main catch |
|---|---|---|---|
| PuppeteerSharp / Playwright | Headless Chromium | Full | You operate the browser fleet |
| DinkToPdf / Rotativa | wkhtmltopdf | No | Archived engine, unpatched SSRF, not thread safe |
| QuestPDF / PdfSharp | Pure .NET document model | Not from HTML | Rebuild templates in code; license above a threshold |
| iText | .NET document model | Limited HTML subset | AGPL; commercial license for closed source |
| IronPDF | Embedded Chromium | Full | Per developer and deployment license cost |
| Rendering API | Managed Chromium | Full | Paid per render; network call |
The bug that hits every C# implementation
Whatever you pick, the single most common failure is rendering before the content is ready. A .NET app fetches data over an API or renders a Blazor or React front end client side, and the PDF fires before the numbers land, so you ship a document with empty tables or a loading spinner frozen in place. Browser based tools solve it with an explicit wait: WaitForSelectorAsync in PuppeteerSharp, a wait condition in Playwright, or a wait_for parameter on an API. Pure document engines sidestep it because they build from data you already have in memory. If your PDFs occasionally come out blank or half filled, this timing gap is almost always why, and the wider set of reasons a render disagrees with the browser is in why your HTML to PDF output does not match the browser.
Many .NET teams hit this while building line-of-business apps that generate invoices and statements from templates. If that is you, and those documents later need to come back as structured data for reconciliation, an automated invoice data extraction step closes the loop from the other direction, turning the PDFs you generate back into fields your accounting system can read.
When to hand the render to an API instead
The library is free until you own the headless Chrome fleet behind it, and in .NET that fleet means Chromium processes, memory caps, a concurrency queue, container dependencies, and a pager. For a low, steady volume of documents that is a fair trade. Past that, or when you would rather your service not run a browser at all, a rendering API takes a URL or raw HTML over HTTPS and returns the PDF, with the fleet as someone else's problem. The full cost comparison, self hosting versus calling out, is in HTML to PDF library vs API.
// One HTTP call from C#, no browser to operate
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
var response = await client.PostAsync(
"https://api.sitepdf.com/v1/render",
new FormUrlEncodedContent(new Dictionary<string, string> {
["url"] = "https://app.example.com/invoices/8412",
["wait_for"] = "#invoice-total",
["format"] = "Letter",
["archive"] = "true",
}));
The render runs in managed Chromium, wait_for holds it until your total is on screen, and archive=true stores a timestamped snapshot of the source page alongside the PDF, which is the record you will want the day a customer disputes an invoice. The PDF generator API documents the full option set, and you can check how a template renders in Chromium right now with the free in-browser converter before writing any integration code.
Which one should you pick?
- Existing HTML templates, modern CSS, and you have ops capacity: PuppeteerSharp or Playwright for .NET, with an explicit wait and a memory cap on the browser pool.
- Existing HTML templates, and you do not want to run a browser: a rendering API, or IronPDF if you prefer an in-process license over a network call.
- The document is defined in code, not HTML: QuestPDF. It is the best modern pure .NET answer; mind the revenue threshold on the license.
- You found DinkToPdf or Rotativa in the project: it works today, but plan the migration. The engine is archived and the SSRF will not be fixed.
- You need the record as much as the file: pick a path that can attach a timestamped copy of what you rendered, because invoices and statements always get questioned eventually.
Whichever you choose, render ten real documents through it before committing, side by side against how Chrome prints the same HTML. Every engine here disagrees with the others somewhere, and finding out where on day one is far cheaper than finding out in a customer's inbox.
Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.