§HOW-TO GUIDE · C# · .NET 8 & 10

HTML to PDF in AWS Lambda with C# (x86_64 and arm64)

The function is packaged as a container image on the AWS Lambda .NET 10 base image: the Chromium engine and its libraries come with the NuGet packages, and the same code runs on x86_64 and arm64.

Compiled and run: AWS Lambda .NET 10 base image, x64 · AWS Lambda .NET 10 base image, ARM64

Video walkthrough · 3:58 · Watch on YouTube · Short · 1:02
§STEP BY STEP

Build it step by step.

  1. 1Create the project

    A class library with the Lambda packages and SelectPdf. NativeRid picks the engine for the function's architecture: linux-x64 for x86_64, linux-arm64 for Graviton. The Dockerfile publishes it onto the AWS Lambda .NET 10 base image; nothing else is installed.

    PdfLambda.csproj
    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <OutputType>Library</OutputType>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
        <!-- linux-x64 for an x86_64 function, linux-arm64 for an arm64 (Graviton) function -->
        <NativeRid Condition="'$(NativeRid)' == ''">linux-x64</NativeRid>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Amazon.Lambda.Core" Version="3.4.0" />
        <PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="3.0.1" />
        <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="3.0.1" />
      </ItemGroup>
    
      <!-- the engine for the function's architecture, and fonts (the Lambda image has none) -->
      <ItemGroup>
        <PackageReference Include="SelectPdf.Universal" Version="26.5.0" />
        <PackageReference Include="SelectPdf.Universal.Native.$(NativeRid)" Version="26.5.0" />
        <PackageReference Include="SelectPdf.Universal.Fonts" Version="26.5.0" />
      </ItemGroup>
    
    </Project>
    Dockerfile
    FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
    ARG NATIVE_RID=linux-x64
    WORKDIR /src
    COPY . .
    RUN dotnet publish PdfLambda.csproj -c Release -r $NATIVE_RID --self-contained false \
        -p:NativeRid=$NATIVE_RID -o /app
    
    # the AWS Lambda base image for .NET 10: nothing else to install
    FROM public.ecr.aws/lambda/dotnet:10
    COPY --from=build /app ${LAMBDA_TASK_ROOT}
    CMD ["PdfLambda::PdfLambda.Function::Handler"]
  2. 2Name the serializer

    Program.cs · lines 1–9
    using Amazon.Lambda.APIGatewayEvents;
    using Amazon.Lambda.Core;
    using SelectPdf.Universal;
    
    [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
    
    namespace PdfLambda;
    
    public class Function

    Lambda passes the request as JSON; DefaultLambdaJsonSerializer reads it. The handler is PdfLambda::PdfLambda.Function::Handler: assembly, type, method, as the Dockerfile's CMD names it.

  3. 3Read the HTML from the request

    Program.cs · lines 11–17
        // a function URL or HTTP API request: the HTML in the body comes back as a PDF
        public async Task<APIGatewayHttpApiV2ProxyResponse> Handler(
            APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
        {
            string html = request.IsBase64Encoded
                ? System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(request.Body))
                : request.Body ?? "<h1>Hello from AWS Lambda</h1>";

    A function URL or an HTTP API passes an APIGatewayHttpApiV2ProxyRequest. The HTML is the request body, base64-encoded when the request says so.

  4. 4Convert

    Program.cs · lines 19–23
            HtmlToPdf converter = new HtmlToPdf();
            converter.Options.DenyLocalFileAccess = true;   // the HTML comes from the caller
            PdfDocument doc = await converter.ConvertHtmlStringAsync(html);
            byte[] pdf = doc.Save();
            doc.Close();

    DenyLocalFileAccess keeps caller-supplied HTML away from the function's files. ConvertHtmlStringAsync renders it and Save() returns the bytes.

  5. 5Return the PDF

    Program.cs · lines 25–34
            // Lambda responses are JSON: the PDF travels base64-encoded (about 4.4 MB of PDF fits in 6 MB)
            return new APIGatewayHttpApiV2ProxyResponse
            {
                StatusCode = 200,
                Headers = new Dictionary<string, string> { ["Content-Type"] = "application/pdf" },
                Body = Convert.ToBase64String(pdf),
                IsBase64Encoded = true,
            };
        }
    }

    Lambda responses are JSON, so the PDF goes base64-encoded with IsBase64Encoded = true; the function URL decodes it and sends the PDF with its content type.

  6. 6Run it

    terminal
    docker build --platform linux/amd64 --provenance=false --sbom=false -t pdf-lambda .
    docker run -p 9000:8080 pdf-lambda
    
    # in a second terminal: send an event to the emulator in the image, decode the PDF
    curl -s http://localhost:9000/2015-03-31/functions/function/invocations \
         -d '{"body":"<h1>Account statement</h1>"}' | jq -r .body | base64 -d > statement.pdf

    The request returns statement.pdf. Without a license key, every page carries a trial watermark; set GlobalProperties.LicenseKey before the first conversion to remove it.

§COMPLETE PROGRAM

The complete Program.cs.

This is the exact file our samples pipeline compiled and ran on AWS Lambda .NET 10 base image, x64 and AWS Lambda .NET 10 base image, ARM64 to produce the output on this page.

Program.cs
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using SelectPdf.Universal;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace PdfLambda;

public class Function
{
    // a function URL or HTTP API request: the HTML in the body comes back as a PDF
    public async Task<APIGatewayHttpApiV2ProxyResponse> Handler(
        APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
    {
        string html = request.IsBase64Encoded
            ? System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(request.Body))
            : request.Body ?? "<h1>Hello from AWS Lambda</h1>";

        HtmlToPdf converter = new HtmlToPdf();
        converter.Options.DenyLocalFileAccess = true;   // the HTML comes from the caller
        PdfDocument doc = await converter.ConvertHtmlStringAsync(html);
        byte[] pdf = doc.Save();
        doc.Close();

        // Lambda responses are JSON: the PDF travels base64-encoded (about 4.4 MB of PDF fits in 6 MB)
        return new APIGatewayHttpApiV2ProxyResponse
        {
            StatusCode = 200,
            Headers = new Dictionary<string, string> { ["Content-Type"] = "application/pdf" },
            Body = Convert.ToBase64String(pdf),
            IsBase64Encoded = true,
        };
    }
}

Open the project on GitHub → this program and the variations below, ready to dotnet run

§DEPLOY

Deploy it.

Push the image to Amazon ECR and create the function from it, with 2048 MB of memory and a 120 second timeout. Replace the account id, region and role with your own. For arm64 (Graviton), build with --platform linux/arm64 --build-arg NATIVE_RID=linux-arm64 and create the function with --architectures arm64.

terminal
docker build --platform linux/amd64 --provenance=false --sbom=false -t pdf-lambda .

aws ecr create-repository --repository-name pdf-lambda
aws ecr get-login-password | docker login --username AWS \
    --password-stdin 123456789012.dkr.ecr.eu-north-1.amazonaws.com
docker tag pdf-lambda 123456789012.dkr.ecr.eu-north-1.amazonaws.com/pdf-lambda:1
docker push 123456789012.dkr.ecr.eu-north-1.amazonaws.com/pdf-lambda:1

aws lambda create-function --function-name pdf-lambda --package-type Image \
    --memory-size 2048 --timeout 120 \
    --code ImageUri=123456789012.dkr.ecr.eu-north-1.amazonaws.com/pdf-lambda:1 \
    --role arn:aws:iam::123456789012:role/pdf-lambda-role
aws lambda create-function-url-config --function-name pdf-lambda --auth-type AWS_IAM

These steps, with the Windows variants, are in the documentation: selectpdf.com/pdf-library/html/AWS-Lambda.htm.

§OUTPUT

The PDF it produces.

First page of statement.pdf, the PDF this sample produces
statement.pdf, page 1

Download statement.pdf ↓ the file this sample produced, unedited

§TROUBLESHOOTING

Common problems and fixes.

Lambda rejects the image

Build with --provenance=false --sbom=false: Lambda accepts only a plain image manifest, not the multi-manifest image Docker builds by default.

The PDF is too large for the response

About 4.4 MB of PDF fits in a 6 MB response. Put larger documents in an S3 bucket and return a presigned link.

Text in other scripts shows as boxes

Lambda images have no fonts: keep SelectPdf.Universal.Fonts in the project.

The first request is slow

Lambda starts a new environment after a few idle minutes, and the first conversion there starts the engine. Give the function 2048 MB: Lambda gives processor time in proportion to memory.

§GOOD TO KNOW

Good to know.

  • The function is a container image: the engine and its libraries are larger than the 250 MB a zip-based function can hold. Source
  • It runs on x86_64 and arm64; arm64 is cheaper per second and converts at a similar speed. Source
  • A warm function with 2048 MB converts selectpdf.com in about 1.5 seconds on x86_64 and 2.3 seconds on arm64. Source
  • A Lambda response is limited to 6 MB and the PDF travels base64-encoded, so about 4.4 MB of PDF fits; store larger ones in Amazon S3. Source
?FAQ

Questions, answered.

Short answers about this guide. The SelectPdf.Universal documentation covers every class and option.

SelectPdf.Universal runs in AWS Lambda as a container image function on the .NET 10 base image, on x86_64 and arm64, with nothing else installed in the image.
The Chromium engine and its libraries are larger than the 250 MB a zip-based function can hold. A container image function has room for them.
Yes. Reference SelectPdf.Universal.Native.linux-arm64 (NativeRid=linux-arm64), build the image with --platform linux/arm64 and create the function with --architectures arm64.
Yes. The Lambda base image includes the runtime interface emulator: run the image with docker run -p 9000:8080 and post an event to http://localhost:9000/2015-03-31/functions/function/invocations.