Skip to content

Run PdfNetStack on AWS Lambda

Since 1.1View as Markdown

Convert HTML to PDF and optimize PDFs in AWS Lambda, compiled ahead of time.

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.

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.

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.

  • 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 /tmp holds about three times the file. Check the size in the S3 event before downloading, and set MaxInputBytes and EphemeralStorage to match. Without Streaming, 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. MaxPages bounds the output.
  • A synchronous response is at most 6 MB, base64 encoded for binary bodies. Write larger PDFs to S3.

On a development machine, measured in a fresh process each time with tools/measure_cold_start.py:

First callNative AOTJIT
HTML to PDF (UseSystemFonts = false)16 ms770 ms
Text extraction of three pages3 ms140 ms
A line of text in an embedded TrueType font5 ms305 ms

Lambda’s own initialization (fetching the code and starting the process) comes on top; the function log reports it as Init Duration.

  • The optimizer, ToArray and a full Save hold the whole document in memory.
  • The converter does not run JavaScript.
  • Native AOT needs a build per operating system and architecture.

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.