Generating PDFs can block your application's main thread for seconds—or even minutes when processing batches. Here's how to fix that with async and multithreading in C#.
using IronPdf;
// Install via NuGet: Install-Package IronPdf
var renderer = new ChromePdfRenderer();
var pdf = await renderer.RenderHtmlAsPdfAsync("<h1>Hello World</h1>");
pdf.SaveAs("output.pdf");
That's it. One await keyword, and your UI stays responsive while the PDF renders in the background.
Why Does Async Matter for PDF Generation?
PDF rendering is CPU-intensive. When you call a synchronous method like RenderHtmlAsPdf, your thread sits there waiting—doing nothing useful—while the rendering engine crunches through HTML, CSS, and JavaScript.
In a web application, that blocked thread could be handling other requests. In a desktop app, your UI freezes. Neither is acceptable in production.
Async programming solves this by freeing the thread during the wait. The rendering still takes the same amount of time, but your application remains responsive.
How Do I Convert HTML to PDF Asynchronously?
IronPDF provides async versions of all rendering methods. The pattern is simple: add Async to the method name and await the call.
using IronPdf;
// Install via NuGet: Install-Package IronPdf
public async Task GeneratePdfAsync()
{
var renderer = new ChromePdfRenderer();
// This won't block your thread
var pdf = await renderer.RenderHtmlAsPdfAsync(@"
<html>
<body>
<h1>Monthly Report</h1>
<p>Generated at: " + DateTime.Now + @"</p>
</body>
</html>");
pdf.SaveAs("report.pdf");
}
The RenderHtmlAsPdfAsync method returns a Task<PdfDocument>. When you await it, execution continues elsewhere until the PDF is ready.
What About Batch Processing?
This is where async really shines. Say you need to generate 100 invoices. The synchronous approach:
// Slow: each PDF waits for the previous one
foreach (var invoice in invoices)
{
var pdf = renderer.RenderHtmlAsPdf(invoice.Html);
pdf.SaveAs($"invoice_{invoice.Id}.pdf");
}
With async, you can process them concurrently:
using IronPdf;
// Install via NuGet: Install-Package IronPdf
var renderer = new ChromePdfRenderer();
var tasks = invoices.Select(async invoice =>
{
var pdf = await renderer.RenderHtmlAsPdfAsync(invoice.Html);
pdf.SaveAs($"invoice_{invoice.Id}.pdf");
});
await Task.WhenAll(tasks);
The Task.WhenAll pattern fires off all rendering tasks simultaneously. On my machine, generating 10 PDFs dropped from 15+ seconds to under 6 seconds.
Should I Use Async or Parallel.ForEach?
Both work. The choice depends on your scenario.
Use async when:
- You're in an ASP.NET Core controller or Blazor component
- You need to keep a UI responsive
- You're already in an async context
Use Parallel.ForEach when:
- You're in a console app or background service
- You want maximum CPU utilization
- You're processing hundreds or thousands of PDFs
Here's the multithreaded approach:
using IronPdf;
// Install via NuGet: Install-Package IronPdf
var results = new ConcurrentBag<PdfDocument>();
Parallel.ForEach(htmlStrings, html =>
{
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
results.Add(pdf);
});
Note the ConcurrentBag—regular List<T> isn't thread-safe. Also note that each iteration creates its own ChromePdfRenderer. Sharing a single renderer across threads works, but separate instances avoid any potential contention.
Is IronPDF Thread-Safe?
Yes. The ChromePdfRenderer class is thread-safe on Windows and Linux. You can share a single instance across threads, though creating separate instances per thread is cleaner and avoids any edge cases.
On macOS, there are some limitations with concurrent rendering. If you're targeting Mac, stick to async over parallel.
How Do I Handle Errors in Async Batch Processing?
When processing batches, one failure shouldn't kill the entire job. Wrap each task in error handling:
using IronPdf;
// Install via NuGet: Install-Package IronPdf
var renderer = new ChromePdfRenderer();
var tasks = invoices.Select(async invoice =>
{
try
{
var pdf = await renderer.RenderHtmlAsPdfAsync(invoice.Html);
pdf.SaveAs($"invoice_{invoice.Id}.pdf");
return (invoice.Id, Success: true, Error: (string)null);
}
catch (Exception ex)
{
return (invoice.Id, Success: false, Error: ex.Message);
}
});
var results = await Task.WhenAll(tasks);
var failures = results.Where(r => !r.Success);
This pattern lets you process everything, then review what failed.
What Performance Gains Can I Expect?
In my testing with IronPDF, processing 10 HTML documents:
| Method | Time |
|---|---|
| Sequential | 15.75s |
| Async (Task.WhenAll) | 5.59s |
| Parallel.ForEach | 5.68s |
That's roughly a 3x improvement. The gains scale with batch size—larger batches see even better relative performance.
The exact numbers depend on your hardware, the complexity of your HTML, and whether you're rendering JavaScript-heavy content. But the pattern holds: concurrent processing dramatically outperforms sequential.
How Do I Combine Async with Progress Reporting?
For long-running batch jobs, users want feedback. Here's how to report progress:
using IronPdf;
// Install via NuGet: Install-Package IronPdf
var renderer = new ChromePdfRenderer();
var progress = new Progress<int>(percent =>
Console.WriteLine($"Progress: {percent}%"));
var completed = 0;
var total = invoices.Count;
var tasks = invoices.Select(async invoice =>
{
var pdf = await renderer.RenderHtmlAsPdfAsync(invoice.Html);
pdf.SaveAs($"invoice_{invoice.Id}.pdf");
var current = Interlocked.Increment(ref completed);
((IProgress<int>)progress).Report(current * 100 / total);
});
await Task.WhenAll(tasks);
The Interlocked.Increment ensures thread-safe counting. The Progress<T> class handles marshaling updates to the UI thread in desktop apps.
What's the Simplest Production-Ready Pattern?
For most applications, this covers everything:
using IronPdf;
// Install via NuGet: Install-Package IronPdf
public class PdfGenerator
{
private readonly ChromePdfRenderer _renderer = new();
public async Task<byte[]> GenerateAsync(string html)
{
var pdf = await _renderer.RenderHtmlAsPdfAsync(html);
return pdf.BinaryData;
}
public async Task GenerateBatchAsync(
IEnumerable<(string Id, string Html)> items,
string outputFolder)
{
var tasks = items.Select(async item =>
{
var pdf = await _renderer.RenderHtmlAsPdfAsync(item.Html);
var path = Path.Combine(outputFolder, $"{item.Id}.pdf");
pdf.SaveAs(path);
});
await Task.WhenAll(tasks);
}
}
Register this as a singleton in your DI container, and you've got async PDF generation throughout your application.
The key takeaway: if you're generating PDFs in C#, there's no reason to block threads anymore. A single await keyword transforms your code from blocking to responsive.
For the complete API reference and more advanced scenarios, check out the official IronPDF async documentation.
Written by Jacob Mellor, CTO at Iron Software. Jacob created IronPDF and leads a team of 50+ engineers building .NET document processing libraries.
Top comments (0)