Run PdfNetStack on AWS Lambda
Convert HTML to PDF and optimize PDFs in AWS Lambda, compiled ahead of time.
Short answer
Section titled “Short answer”Publish the function with Native AOT for the provided.al2023 runtime. Package the fonts your
HTML uses with the function, load them once per instance, and turn installed fonts off. Create
the options once and share them: every invocation the instance serves uses them.
// Created once per instance and shared by every invocation, concurrent ones included.internal static class Pdfs{ private static readonly HtmlConverterOptions Options = new() { Fonts = LoadFonts(), UseSystemFonts = false, // the host has no fonts, or ones that can change MaxPages = 200, };
public static byte[] Convert(string html) => HtmlConverter.ConvertToPdf(html, Options);
// The fonts deployed with the function, in fonts/ next to the executable. private static PdfFontCollection LoadFonts() { var fonts = new PdfFontCollection(); fonts.AddDirectory(Path.Combine(AppContext.BaseDirectory, "fonts")); return fonts; }}A complete function with an S3 trigger, a function URL and a SAM template is in samples/serverless/aws-lambda.
Native AOT
Section titled “Native AOT”PdfNetStack, PdfNetStack.Html and PdfNetStack.Ocr are marked AOT compatible and publish with
Native AOT and trimming with no warnings. CI publishes a smoke test app for linux-x64 and
linux-arm64 that roots all three libraries, so a warning anywhere in them fails the build, and
runs it. Parsing and output do not depend on the culture, so InvariantGlobalization, which
the Lambda AOT templates set, changes nothing.
Native AOT compiles for one operating system and architecture, and links against the C library of the machine that builds it. Build on Amazon Linux 2023 for the architecture the function runs on; the sample has a Dockerfile for that.
Lambda has no system fonts. With UseSystemFonts = false the output depends only on the HTML and
the fonts you packaged, and the converter never scans the file system for fonts. CSS families
that are not packaged fall back to the standard 14 fonts (Helvetica, Times, Courier), which cover
Western European text. A font loaded once is thread safe and shared by every conversion; variable
font instances made for a conversion are released once nothing uses them. See
Fonts and assets.
Without a resource loader, the converter fetches nothing from the network: put images and style
sheets inline, as data: URIs and <style> elements.
Optimizing files from S3
Section titled “Optimizing files from S3”The optimizer reads its input stream to the end and holds the document in memory. Write the
result to /tmp, the only writable directory, and upload it from there:
internal static class Optimizer{ public static async Task<PdfOptimizationResult> OptimizeAsync(Stream input, Stream destination, CancellationToken cancellationToken) { // /tmp on AWS Lambda, the instance's local storage on Azure Functions; deleted on close. string temp = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".pdf"); await using var output = new FileStream(temp, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 1 << 16, FileOptions.Asynchronous | FileOptions.DeleteOnClose);
var options = new PdfOptimizerOptions { Streaming = true, // flat memory; about 3x the file in the temp folder MaxInputBytes = 600L << 20, // larger inputs pass through unchanged MaxDegreeOfParallelism = Environment.ProcessorCount, // the default; vCPUs grow with memory on Lambda }; PdfOptimizationResult result = await PdfOptimizer.OptimizeAsync(input, output, options, cancellationToken);
output.Position = 0; await output.CopyToAsync(destination, cancellationToken); // or hand it to the storage SDK's upload return result; }}The library itself writes nothing to disk unless you give it a path or a stream.
Memory and CPU
Section titled “Memory and CPU”- Lambda gives one vCPU at 1,769 MB of memory and more above. The optimizer recompresses streams
on every core it gets (
MaxDegreeOfParallelism), so more memory also means shorter runs. - With
Streaming, the optimizer’s memory stays flat whatever the file size (a generated 278 MB file peaked at about 460 MB of working set), and/tmpholds about three times the file. Check the size in the S3 event before downloading, and setMaxInputBytesandEphemeralStorageto match. WithoutStreaming, plan for about ten times the file size in memory. - HTML conversion of invoices and reports fits in 512 MB; 1,024 MB converts faster for the CPU.
MaxPagesbounds the output. - A synchronous response is at most 6 MB, base64 encoded for binary bodies. Write larger PDFs to S3.
Cold start
Section titled “Cold start”On a development machine, measured in a fresh process each time with
tools/measure_cold_start.py:
| First call | Native AOT | JIT |
|---|---|---|
HTML to PDF (UseSystemFonts = false) | 16 ms | 770 ms |
| Text extraction of three pages | 3 ms | 140 ms |
| A line of text in an embedded TrueType font | 5 ms | 305 ms |
Lambda’s own initialization (fetching the code and starting the process) comes on top; the
function log reports it as Init Duration.
Limits
Section titled “Limits”- The optimizer,
ToArrayand a fullSavehold the whole document in memory. - The converter does not run JavaScript.
- Native AOT needs a build per operating system and architecture.
Related
Section titled “Related”Questions
- Does PdfNetStack work with Native AOT on AWS Lambda?
- Yes. The core, HTML and OCR libraries publish with Native AOT and trimming with no warnings, and CI runs a Native AOT build on Linux x64 and arm64. A function on the provided.al2023 runtime starts without JIT compilation.
- Why does HTML to PDF in Lambda use the wrong font?
- Lambda has no system fonts. Package the fonts the HTML names with the function, load them into a PdfFontCollection and set UseSystemFonts to false. Families that are not packaged fall back to Helvetica, Times or Courier.