COMPARISON

Free PDF Libraries for .NET: Hidden Costs and Better Alternatives in C#

Free PDF libraries for .NET come with hidden costs: AGPL licensing traps, missing HTML support, deprecated dependencies with unpatched CVEs, revenue thresholds, and operational complexity that often exceeds commercial license costs.

Before you commit to any of them, run this in a terminal:


<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>

<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
HTML

If your .NET application uses wkhtmltopdf or any of its wrappers — DinkToPdf, TuesPechkin, Rotativa, NReco.PdfGenerator — that HTML will execute a Server-Side Request Forgery against your cloud provider's metadata endpoint. AWS IAM credentials, Azure managed identity tokens, GCP service account keys. All exposed. The project was archived January 2023. No patch is coming.

That is what "free" costs in production.

Quickstart: Evaluate PDF Libraries for Your .NET Project

// Install: dotnet add package IronPdf
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><style>h1{font-family:Inter}</style>");
pdf.SaveAs("output.pdf");
// Install: dotnet add package IronPdf
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><style>h1{font-family:Inter}</style>");
pdf.SaveAs("output.pdf");
' Install: dotnet add package IronPdf
Dim renderer As New ChromePdfRenderer()
Dim pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><style>h1{font-family:Inter}</style>")
pdf.SaveAs("output.pdf")
$vbLabelText   $csharpLabel
var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);
gfx.DrawString("Hello World", new XFont("Arial", 20), XBrushes.Black, 72, 72);
document.Save("output.pdf");
var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);
gfx.DrawString("Hello World", new XFont("Arial", 20), XBrushes.Black, 72, 72);
document.Save("output.pdf");
Dim document As New PdfDocument()
Dim page = document.AddPage()
Dim gfx = XGraphics.FromPdfPage(page)
gfx.DrawString("Hello World", New XFont("Arial", 20), XBrushes.Black, 72, 72)
document.Save("output.pdf")
$vbLabelText   $csharpLabel

The gap between these two examples is the gap between what most .NET developers need and what most free libraries provide.

What Does "Free" Actually Mean in .NET PDF Libraries?

Search NuGet for "PDF" and you will find libraries across five licensing models, each with different constraints for .NET deployment:

MIT/Apache (genuinely permissive): PdfSharp. Use it in commercial applications, Docker containers, Azure Functions, AWS Lambda — no restrictions, no revenue thresholds, no source code disclosure. The catch: it cannot convert HTML to PDF.

AGPL (copyleft trap): iText Core (formerly iTextSharp). Deploy it in any network-accessible application — web apps, REST APIs, microservices — and you must release your entire source code under AGPL. SaaS companies get no exemption.

Revenue-gated: QuestPDF Community License. Free below $1M annual gross revenue. Cross that threshold and you need a commercial license. The transition is not gradual — it is a cliff.

Abandoned: wkhtmltopdf and all .NET wrappers. Archived with unpatched CVEs scoring 9.8 Critical. Zero security maintenance. A liability in any compliance audit.

Operationally expensive: PuppeteerSharp and Playwright for .NET. No licensing restrictions, full modern CSS support — but you are managing external browser processes, Chromium downloads, and memory lifecycle in production.

Each category creates different risks for .NET deployments. The rest of this article breaks down those risks with code, numbers, and NuGet ecosystem data.

Why Is iText's Pricing the Real Story?

Most articles about iText focus on the AGPL enforcement angle. That is covered elsewhere. The more relevant question for .NET teams evaluating PDF libraries is what happens when you need a commercial license.

What Does Commercial iText Actually Cost?

In April 2020, iText transitioned from perpetual licensing to subscription-based models. Third-party pricing data from Vendr's transaction database shows:

  • Average annual contract: ~$45,000
  • High-end contracts: Up to $210,000 depending on PDF volume
  • Pricing model: Volume-based — costs scale with how many PDFs your application generates annually

That volume-based model creates unpredictable budgets for growing applications. A .NET microservice generating 10,000 PDFs per month during Q1 that scales to 100,000 during Q4 will see a licensing bill that scales with it — and iText's pricing tiers are not public.

Compare that to IronPDF's published pricing: $749 for a perpetual Lite license. No annual subscription. No volume metering. No surprises when your application scales.

How Does the Subscription Model Affect .NET Teams?

The shift from perpetual to subscription licensing changes the TCO calculation:

FactoriText SubscriptionIronPDF Perpetual
Year 1 cost~$45,000$749 - $2,999
Year 3 cost~$135,000$749 - $2,999 (one-time)
Volume scalingCosts increaseNo volume metering
Budget predictabilityVariableFixed
Cancellation riskLose accessPerpetual ownership

For a .NET team building a SaaS product, the five-year delta can exceed $200,000. That is the pricing story that matters more than AGPL enforcement debates.

What Are PdfSharp's Limitations for .NET Deployments?

PdfSharp is the exception among free libraries: MIT license, 34+ million NuGet downloads, genuinely permissive for commercial use. No revenue thresholds. No source code disclosure.

The limitation is architectural. PdfSharp operates at the PDF coordinate level. There is no HTML parser, no CSS engine, no DOM rendering.

var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);

// Every element needs manual coordinates
var titleFont = new XFont("Arial", 18, XFontStyleEx.Bold);
var bodyFont = new XFont("Arial", 10);

gfx.DrawString("INVOICE #2024-0847", titleFont, XBrushes.Black, 72, 72);
gfx.DrawString("Date: 2024-12-15", bodyFont, XBrushes.Gray, 72, 100);

// Table header - manual line drawing
gfx.DrawLine(XPens.Black, 72, 140, 540, 140);
gfx.DrawString("Item", bodyFont, XBrushes.Black, 72, 155);
gfx.DrawString("Qty", bodyFont, XBrushes.Black, 300, 155);
gfx.DrawString("Price", bodyFont, XBrushes.Black, 400, 155);
gfx.DrawLine(XPens.Black, 72, 170, 540, 170);

// Row data - every cell is a manual coordinate
gfx.DrawString("Annual License", bodyFont, XBrushes.Black, 72, 185);
gfx.DrawString("1", bodyFont, XBrushes.Black, 300, 185);
gfx.DrawString("$749.00", bodyFont, XBrushes.Black, 400, 185);

document.Save("invoice.pdf");
var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);

// Every element needs manual coordinates
var titleFont = new XFont("Arial", 18, XFontStyleEx.Bold);
var bodyFont = new XFont("Arial", 10);

gfx.DrawString("INVOICE #2024-0847", titleFont, XBrushes.Black, 72, 72);
gfx.DrawString("Date: 2024-12-15", bodyFont, XBrushes.Gray, 72, 100);

// Table header - manual line drawing
gfx.DrawLine(XPens.Black, 72, 140, 540, 140);
gfx.DrawString("Item", bodyFont, XBrushes.Black, 72, 155);
gfx.DrawString("Qty", bodyFont, XBrushes.Black, 300, 155);
gfx.DrawString("Price", bodyFont, XBrushes.Black, 400, 155);
gfx.DrawLine(XPens.Black, 72, 170, 540, 170);

// Row data - every cell is a manual coordinate
gfx.DrawString("Annual License", bodyFont, XBrushes.Black, 72, 185);
gfx.DrawString("1", bodyFont, XBrushes.Black, 300, 185);
gfx.DrawString("$749.00", bodyFont, XBrushes.Black, 400, 185);

document.Save("invoice.pdf");
Imports PdfSharp.Pdf
Imports PdfSharp.Drawing

Dim document As New PdfDocument()
Dim page As PdfPage = document.AddPage()
Dim gfx As XGraphics = XGraphics.FromPdfPage(page)

' Every element needs manual coordinates
Dim titleFont As New XFont("Arial", 18, XFontStyleEx.Bold)
Dim bodyFont As New XFont("Arial", 10)

gfx.DrawString("INVOICE #2024-0847", titleFont, XBrushes.Black, 72, 72)
gfx.DrawString("Date: 2024-12-15", bodyFont, XBrushes.Gray, 72, 100)

' Table header - manual line drawing
gfx.DrawLine(XPens.Black, 72, 140, 540, 140)
gfx.DrawString("Item", bodyFont, XBrushes.Black, 72, 155)
gfx.DrawString("Qty", bodyFont, XBrushes.Black, 300, 155)
gfx.DrawString("Price", bodyFont, XBrushes.Black, 400, 155)
gfx.DrawLine(XPens.Black, 72, 170, 540, 170)

' Row data - every cell is a manual coordinate
gfx.DrawString("Annual License", bodyFont, XBrushes.Black, 72, 185)
gfx.DrawString("1", bodyFont, XBrushes.Black, 300, 185)
gfx.DrawString("$749.00", bodyFont, XBrushes.Black, 400, 185)

document.Save("invoice.pdf")
$vbLabelText   $csharpLabel

That is 20 lines for a single-row invoice with no styling, no responsive layout, no CSS. Now imagine building a 15-page compliance report with dynamic data, charts, and corporate branding.

Cross-Platform Deployment Considerations

PdfSharp works well in .NET 6+ cross-platform scenarios — it has no native dependencies, no Chromium binary, no external processes. It deploys cleanly to Docker, Azure Functions, and AWS Lambda with minimal container size.

For applications that need only programmatic PDF creation from structured data — shipping labels, simple receipts, coordinate-plotted diagrams — PdfSharp is a legitimate choice. Its NuGet health is strong: active commits, responsive maintainer, regular releases.

For anything involving HTML content, web templates, or modern CSS, PdfSharp is the wrong tool.

When Does QuestPDF Stop Being Free?

QuestPDF took a different design approach from PdfSharp: a fluent API that reads like a layout description rather than coordinate math. The API design is genuinely good.

Document.Create(container =>
{
    container.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(2, Unit.Centimetre);

        page.Content().Column(column =>
        {
            column.Item().Text("Invoice #2024-0847").FontSize(18).Bold();
            column.Item().Table(table =>
            {
                table.ColumnsDefinition(columns =>
                {
                    columns.RelativeColumn(3);
                    columns.RelativeColumn(1);
                    columns.RelativeColumn(1);
                });

                table.Cell().Text("Item").Bold();
                table.Cell().Text("Qty").Bold();
                table.Cell().Text("Price").Bold();

                table.Cell().Text("Annual License");
                table.Cell().Text("1");
                table.Cell().Text("$749.00");
            });
        });
    });
}).GeneratePdf("invoice.pdf");
Document.Create(container =>
{
    container.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(2, Unit.Centimetre);

        page.Content().Column(column =>
        {
            column.Item().Text("Invoice #2024-0847").FontSize(18).Bold();
            column.Item().Table(table =>
            {
                table.ColumnsDefinition(columns =>
                {
                    columns.RelativeColumn(3);
                    columns.RelativeColumn(1);
                    columns.RelativeColumn(1);
                });

                table.Cell().Text("Item").Bold();
                table.Cell().Text("Qty").Bold();
                table.Cell().Text("Price").Bold();

                table.Cell().Text("Annual License");
                table.Cell().Text("1");
                table.Cell().Text("$749.00");
            });
        });
    });
}).GeneratePdf("invoice.pdf");
Document.Create(Sub(container)
    container.Page(Sub(page)
        page.Size(PageSizes.A4)
        page.Margin(2, Unit.Centimetre)

        page.Content().Column(Sub(column)
            column.Item().Text("Invoice #2024-0847").FontSize(18).Bold()
            column.Item().Table(Sub(table)
                table.ColumnsDefinition(Sub(columns)
                    columns.RelativeColumn(3)
                    columns.RelativeColumn(1)
                    columns.RelativeColumn(1)
                End Sub)

                table.Cell().Text("Item").Bold()
                table.Cell().Text("Qty").Bold()
                table.Cell().Text("Price").Bold()

                table.Cell().Text("Annual License")
                table.Cell().Text("1")
                table.Cell().Text("$749.00")
            End Sub)
        End Sub)
    End Sub)
End Sub).GeneratePdf("invoice.pdf")
$vbLabelText   $csharpLabel

That is more expressive than PdfSharp's coordinate system. But it shares the same fundamental limitation: no HTML rendering.

The Revenue Cliff

QuestPDF's Community License is free for companies with annual gross revenue below $1,000,000. Cross that threshold and you need a Professional ($699/year) or Enterprise ($1,999/year) license.

For a startup, this creates a growth timeline scenario:

  • Year 1 ($200K revenue): Free. QuestPDF's fluent API accelerates initial development.
  • Year 2 ($600K revenue): Still free. The API is deeply integrated into your codebase.
  • Year 3 ($1.1M revenue): License required. You are now locked into the API with significant switching costs.

The transition is not about the license cost — it is about the switching cost you have accumulated. By year 3, your PDF generation layer may span dozens of files with fluent API calls that have no equivalent in other libraries.

The HTML Misconception

Developers coming from web frameworks expect that a modern .NET PDF library will accept HTML input. QuestPDF explicitly does not support HTML-to-PDF conversion. Its API is code-only — every layout element is a method call, not markup.

This mismatch catches teams that purchase QuestPDF licenses (or build on the Community edition) only to discover mid-project that their existing HTML invoice templates, email-to-PDF workflows, or report generators cannot use QuestPDF at all.

IronPDF accepts HTML, CSS, and JavaScript as input because it embeds a Chromium rendering engine. The same HTML that renders in Chrome renders identically in a PDF.

Why Should You Avoid wkhtmltopdf in Any .NET Deployment?

I opened this article with CVE-2022-35583 for a reason. That SSRF vulnerability is not theoretical — proof-of-concept exploits are publicly available and actively used.

The Full Security Picture

wkhtmltopdf carries two unpatched CVEs that will never be fixed:

CVE-2022-35583 (CVSS 9.8 Critical): Server-Side Request Forgery via iframe injection. In cloud environments, this exposes instance metadata endpoints — AWS IAM credentials, Azure managed identity tokens, GCP service account keys, Kubernetes service account tokens.

CVE-2020-21365 (CVSS 7.5 High): Directory traversal allowing remote attackers to read local files through crafted HTML input.

Both vulnerabilities have public exploit code. Both are actively exploited. Neither will receive a patch.

.NET Wrapper Ecosystem Health

Every .NET wrapper for wkhtmltopdf inherits these vulnerabilities and adds its own maintenance debt:

WrapperLast Meaningful CommitOpen Issues.NET 8 Support
DinkToPdf2018300+ unansweredNo
TuesPechkin2015AbandonedNo
Rotativa2019MVC-onlyNo
NReco.PdfGeneratorActiveCommercialLimited

NReco is the only actively maintained wrapper, but it still depends on the wkhtmltopdf binary — which means the CVEs travel with it.

Rendering Is Frozen at 2013

Beyond security, wkhtmltopdf's Qt WebKit engine is frozen at 2013-era web standards. No CSS Flexbox. No CSS Grid. No CSS Variables. No calc(). ES6+ JavaScript fails to execute.

Any .NET application using Tailwind CSS, Bootstrap 5, or modern CSS frameworks will produce broken output. For a .NET 8 application targeting containerized deployment, an unmaintained binary with no modern web standards support is technical debt you are choosing to carry.

What Is the True Operational Cost of PuppeteerSharp?

This is the section most "free PDF library" articles get wrong. PuppeteerSharp and Playwright for .NET are technically excellent — they render HTML through real Chromium, supporting every CSS feature and JavaScript API. No licensing restrictions. No revenue thresholds.

The cost is operational. Here is what production PuppeteerSharp PDF generation actually looks like:

using PuppeteerSharp;

public class PuppeteerPdfService : IDisposable
{
    private IBrowser _browser;
    private readonly SemaphoreSlim _semaphore;
    private long _pdfCount = 0;

    public PuppeteerPdfService()
    {
        // Limit concurrent pages to prevent memory exhaustion
        _semaphore = new SemaphoreSlim(3, 3);
    }

    public async Task InitializeAsync()
    {
        // Step 1: Download Chromium binary (~280MB)
        var fetcher = new BrowserFetcher();
        await fetcher.DownloadAsync();

        // Step 2: Launch with production-hardened flags
        _browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[]
            {
                "--no-sandbox",              // Required in Docker
                "--disable-setuid-sandbox",  // Required in Docker
                "--disable-dev-shm-usage",   // Prevent /dev/shm exhaustion
                "--disable-gpu",
                "--no-zygote",
                "--single-process",
                "--disable-extensions",
                "--max_old_space_size=4096"
            },
            Timeout = 30000
        });
    }

    public async Task<byte[]> GeneratePdfAsync(string html)
    {
        await _semaphore.WaitAsync();
        IPage page = null;

        try
        {
            Interlocked.Increment(ref _pdfCount);
            page = await _browser.NewPageAsync();

            await page.SetContentAsync(html, new NavigationOptions
            {
                WaitUntil = new[] { WaitUntilNavigation.Networkidle0 },
                Timeout = 20000
            });

            var pdfBytes = await page.PdfAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true,
                MarginOptions = new MarginOptions
                {
                    Top = "20mm", Bottom = "20mm",
                    Left = "15mm", Right = "15mm"
                }
            });

            return pdfBytes;
        }
        finally
        {
            if (page != null)
            {
                try { await page.CloseAsync(); }
                catch { /* Disposal can hang — GitHub issue #1489 */ }
            }
            _semaphore.Release();
        }
    }

    // Restart browser periodically to reclaim leaked memory
    public async Task RecycleBrowserAsync()
    {
        var oldBrowser = _browser;
        await InitializeAsync();
        try { oldBrowser?.Dispose(); } catch { }
    }

    public void Dispose()
    {
        _browser?.Dispose();
        _semaphore?.Dispose();
    }
}
using PuppeteerSharp;

public class PuppeteerPdfService : IDisposable
{
    private IBrowser _browser;
    private readonly SemaphoreSlim _semaphore;
    private long _pdfCount = 0;

    public PuppeteerPdfService()
    {
        // Limit concurrent pages to prevent memory exhaustion
        _semaphore = new SemaphoreSlim(3, 3);
    }

    public async Task InitializeAsync()
    {
        // Step 1: Download Chromium binary (~280MB)
        var fetcher = new BrowserFetcher();
        await fetcher.DownloadAsync();

        // Step 2: Launch with production-hardened flags
        _browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[]
            {
                "--no-sandbox",              // Required in Docker
                "--disable-setuid-sandbox",  // Required in Docker
                "--disable-dev-shm-usage",   // Prevent /dev/shm exhaustion
                "--disable-gpu",
                "--no-zygote",
                "--single-process",
                "--disable-extensions",
                "--max_old_space_size=4096"
            },
            Timeout = 30000
        });
    }

    public async Task<byte[]> GeneratePdfAsync(string html)
    {
        await _semaphore.WaitAsync();
        IPage page = null;

        try
        {
            Interlocked.Increment(ref _pdfCount);
            page = await _browser.NewPageAsync();

            await page.SetContentAsync(html, new NavigationOptions
            {
                WaitUntil = new[] { WaitUntilNavigation.Networkidle0 },
                Timeout = 20000
            });

            var pdfBytes = await page.PdfAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true,
                MarginOptions = new MarginOptions
                {
                    Top = "20mm", Bottom = "20mm",
                    Left = "15mm", Right = "15mm"
                }
            });

            return pdfBytes;
        }
        finally
        {
            if (page != null)
            {
                try { await page.CloseAsync(); }
                catch { /* Disposal can hang — GitHub issue #1489 */ }
            }
            _semaphore.Release();
        }
    }

    // Restart browser periodically to reclaim leaked memory
    public async Task RecycleBrowserAsync()
    {
        var oldBrowser = _browser;
        await InitializeAsync();
        try { oldBrowser?.Dispose(); } catch { }
    }

    public void Dispose()
    {
        _browser?.Dispose();
        _semaphore?.Dispose();
    }
}
Imports PuppeteerSharp
Imports System.Threading

Public Class PuppeteerPdfService
    Implements IDisposable

    Private _browser As IBrowser
    Private ReadOnly _semaphore As SemaphoreSlim
    Private _pdfCount As Long = 0

    Public Sub New()
        ' Limit concurrent pages to prevent memory exhaustion
        _semaphore = New SemaphoreSlim(3, 3)
    End Sub

    Public Async Function InitializeAsync() As Task
        ' Step 1: Download Chromium binary (~280MB)
        Dim fetcher = New BrowserFetcher()
        Await fetcher.DownloadAsync()

        ' Step 2: Launch with production-hardened flags
        _browser = Await Puppeteer.LaunchAsync(New LaunchOptions With {
            .Headless = True,
            .Args = {
                "--no-sandbox",              ' Required in Docker
                "--disable-setuid-sandbox",  ' Required in Docker
                "--disable-dev-shm-usage",   ' Prevent /dev/shm exhaustion
                "--disable-gpu",
                "--no-zygote",
                "--single-process",
                "--disable-extensions",
                "--max_old_space_size=4096"
            },
            .Timeout = 30000
        })
    End Function

    Public Async Function GeneratePdfAsync(html As String) As Task(Of Byte())
        Await _semaphore.WaitAsync()
        Dim page As IPage = Nothing

        Try
            Interlocked.Increment(_pdfCount)
            page = Await _browser.NewPageAsync()

            Await page.SetContentAsync(html, New NavigationOptions With {
                .WaitUntil = {WaitUntilNavigation.Networkidle0},
                .Timeout = 20000
            })

            Dim pdfBytes = Await page.PdfAsync(New PdfOptions With {
                .Format = PaperFormat.A4,
                .PrintBackground = True,
                .MarginOptions = New MarginOptions With {
                    .Top = "20mm", .Bottom = "20mm",
                    .Left = "15mm", .Right = "15mm"
                }
            })

            Return pdfBytes
        Finally
            If page IsNot Nothing Then
                Try
                    Await page.CloseAsync()
                Catch
                    ' Disposal can hang — GitHub issue #1489
                End Try
            End If
            _semaphore.Release()
        End Try
    End Function

    ' Restart browser periodically to reclaim leaked memory
    Public Async Function RecycleBrowserAsync() As Task
        Dim oldBrowser = _browser
        Await InitializeAsync()
        Try
            oldBrowser?.Dispose()
        Catch
        End Try
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        _browser?.Dispose()
        _semaphore?.Dispose()
    End Sub
End Class
$vbLabelText   $csharpLabel

That is 80+ lines before you have generated a single PDF. And it is still missing error recovery, health checks, metrics, and the memory recycling timer that production systems need.

Why This Complexity Matters for .NET Deployments

The operational burden scales with deployment target:

Docker: You must include Chromium in your container image. That adds ~280MB to the image, increasing pull times, registry storage costs, and cold-start latency. Your Dockerfile needs explicit apt-get install commands for Chromium's system dependencies — libgbm-dev, libasound2, libatk-bridge2.0-0, and roughly 15 others that vary by base image.

Azure Functions / AWS Lambda: Serverless environments constrain memory and execution time. Chromium's cold start — downloading and launching the browser process — can consume 5-10 seconds and 500MB+ of memory. Lambda's 250MB deployment package limit means Chromium barely fits, and Azure Consumption plan's 1.5GB memory cap leaves little room for the actual PDF generation.

Kubernetes: Browser processes do not play well with container orchestration. Memory limits that look fine for your application code become insufficient when Chromium spawns renderer processes. Pod OOMKills become a regular occurrence unless you set memory requests significantly higher than your application actually needs.

The Equivalent IronPDF Code

var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4
renderer.RenderingOptions.MarginTop = 20
renderer.RenderingOptions.MarginBottom = 20

Dim pdf = renderer.RenderHtmlAsPdf(html)
pdf.SaveAs("output.pdf")
$vbLabelText   $csharpLabel

Six lines. IronPDF embeds Chromium internally — browser lifecycle, memory management, and process pooling are handled by the library. No SemaphoreSlim. No browser recycling. No Dockerfile modifications. The NuGet package includes everything.

The trade-off is real: IronPDF's NuGet package is larger than PdfSharp's because it includes the Chromium binary. First-PDF latency is 2-5 seconds while the engine initializes, then 100-500ms for subsequent generations. For applications where deployment size is the primary constraint, that matters. For applications where developer time and operational reliability matter more, the embedded approach wins.

.NET Ecosystem Health: NuGet Package Comparison

Before choosing a library, check its NuGet ecosystem health. Dependency chains, release frequency, and issue resolution time tell you more than feature lists:

LibraryNuGet DownloadsLast ReleaseOpen Issues.NET 8 TFMNative Dependencies
PdfSharp34M+ActiveLowNone
QuestPDF8M+ActiveLowNone
iText Core30M+ActiveModerateNone
IronPDF10M+ActiveLowChromium (bundled)
DinkToPdf5M+2018300+wkhtmltopdf binary
PuppeteerSharp15M+ActiveModerateChromium (external)

High download counts on abandoned packages like DinkToPdf reflect legacy adoption, not current health. The 300+ open issues with no responses tell the real story.

For .NET 8 applications targeting net8.0 TFM: PdfSharp, QuestPDF, iText Core, and IronPDF all support it natively. wkhtmltopdf wrappers do not — expect DllNotFoundException and NU1202 target framework incompatibility errors.

Decision Matrix

RequirementPdfSharpQuestPDFiText CorewkhtmltopdfPuppeteerSharpIronPDF
Truly free (MIT/permissive)❌ Revenue gate❌ AGPL⚠️ Abandoned❌ Commercial
HTML to PDF⚠️ Limited⚠️ Broken CSS
Modern CSS (Flexbox/Grid)
JavaScript execution⚠️ ES5 only
No browser management
Active security patches
No revenue thresholdN/AN/A
Predictable licensing costFreeCliff at $1M~$45K/yr avgN/AFree$749 perpetual
Docker-friendly✅ Small✅ Small✅ Small⚠️ Binary dep⚠️ +280MB✅ Self-contained
Serverless-compatible⚠️ Cold start

If you need only programmatic PDF creation from structured data: PdfSharp (MIT, no restrictions) or QuestPDF (better API, watch the revenue threshold).

If you need HTML-to-PDF with modern CSS and do not want to manage browser infrastructure: IronPDF. Embedded Chromium handles the rendering engine lifecycle. Published pricing at a fraction of iText's subscription model.

If you need HTML-to-PDF and are comfortable managing browser processes: PuppeteerSharp gives you full control. Budget for the operational overhead.

If you are currently using wkhtmltopdf or any of its wrappers: Migrate. The security exposure alone justifies the effort — and every month you delay, the CVE list stays unpatched.