Invalid Date

When you need to generate PDFs in a serverless architecture, bundling Chromium into a Lambda function is impractical. You hit deployment package size limits, suffer from slow cold starts, and consume excessive memory.

By delegating PDF generation to an API, your Lambda function only needs to send a lightweight HTTP request. This guide shows you how to call the FUNBREW PDF API from AWS Lambda to generate PDFs serverlessly, with code examples in Node.js and Python.

For the basics of the API itself, see the quickstart guide.

Why Use a PDF API in Serverless

Problems with Self-Hosted PDF Generation on Lambda

Issue Details
Deployment size Chromium bundle is ~130MB, close to Lambda's 250MB limit
Cold starts Heavier packages mean slower first invocations (5-10+ seconds)
Memory usage Chromium alone requires 500MB+ to launch
Concurrency You hit concurrent execution limits quickly

Benefits of API-Based PDF Generation

Benefit Details
Lightweight deploys Just an HTTP client, a few KB
Fast cold starts Under 100ms
Low memory 128MB is sufficient
Unlimited scale The API handles parallel processing
Cost savings Minimize memory allocation and execution time

Setup

Managing API Keys

To securely manage API keys in Lambda, use AWS Secrets Manager or encrypted environment variables.

# Store the key in Secrets Manager via AWS CLI
aws secretsmanager create-secret \
  --name funbrew-pdf-api-key \
  --secret-string "sk-your-api-key"

For more on API key security, see the security guide.

IAM Role Configuration

Your Lambda function needs the following permissions:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": "arn:aws:secretsmanager:ap-northeast-1:*:secret:funbrew-pdf-api-key-*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::your-pdf-bucket/*"
    }
  ]
}

Node.js Lambda Function

Basic PDF Generation

const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');

const secretsManager = new SecretsManagerClient({ region: 'ap-northeast-1' });
const s3 = new S3Client({ region: 'ap-northeast-1' });

let cachedApiKey = null;

async function getApiKey() {
  if (cachedApiKey) return cachedApiKey;

  const command = new GetSecretValueCommand({ SecretId: 'funbrew-pdf-api-key' });
  const response = await secretsManager.send(command);
  cachedApiKey = response.SecretString;
  return cachedApiKey;
}

exports.handler = async (event) => {
  const apiKey = await getApiKey();

  // Call the PDF generation API
  const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      html: event.html || '<h1>Hello from Lambda</h1>',
      engine: 'quality',
      format: 'A4',
    }),
  });

  if (!response.ok) {
    const errorBody = await response.text();
    throw new Error(`PDF API error: ${response.status} - ${errorBody}`);
  }

  const pdfBuffer = Buffer.from(await response.arrayBuffer());

  // Upload to S3
  const key = `pdfs/${Date.now()}.pdf`;
  await s3.send(new PutObjectCommand({
    Bucket: process.env.PDF_BUCKET,
    Key: key,
    Body: pdfBuffer,
    ContentType: 'application/pdf',
  }));

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'PDF generated and uploaded',
      s3Key: key,
      size: pdfBuffer.length,
    }),
  };
};

Template-Based PDF Generation

Separating HTML templates from data makes your Lambda function more versatile. For template design patterns, see the template engine guide.

exports.handler = async (event) => {
  const { templateId, data } = event;
  const apiKey = await getApiKey();

  // Inject dynamic data into template
  const html = buildHtml(templateId, data);

  const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ html, engine: 'quality', format: 'A4' }),
  });

  if (!response.ok) {
    throw new Error(`PDF API error: ${response.status}`);
  }

  // Upload to S3, etc.
  // ...
};

function buildHtml(templateId, data) {
  const templates = {
    invoice: (d) => `
      <html>
      <head><style>
        body { font-family: 'Noto Sans JP', sans-serif; padding: 40px; }
        .header { display: flex; justify-content: space-between; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
      </style></head>
      <body>
        <div class="header">
          <h1>Invoice</h1>
          <p>No. ${d.invoiceNumber}</p>
        </div>
        <p>Date: ${d.date}</p>
        <p>Customer: ${d.customerName}</p>
        <table>
          <tr><th>Item</th><th>Qty</th><th>Unit Price</th><th>Amount</th></tr>
          ${d.items.map(i => `<tr><td>${i.name}</td><td>${i.qty}</td><td>${i.price}</td><td>${i.qty * i.price}</td></tr>`).join('')}
        </table>
      </body>
      </html>
    `,
  };

  return templates[templateId](data);
}

For more on invoice PDF automation, see the invoice automation guide.

Python Lambda Function

import json
import os
import time
import boto3
import urllib.request

secrets_client = boto3.client('secretsmanager', region_name='ap-northeast-1')
s3_client = boto3.client('s3', region_name='ap-northeast-1')

cached_api_key = None

def get_api_key():
    global cached_api_key
    if cached_api_key:
        return cached_api_key

    response = secrets_client.get_secret_value(SecretId='funbrew-pdf-api-key')
    cached_api_key = response['SecretString']
    return cached_api_key

def handler(event, context):
    api_key = get_api_key()
    html = event.get('html', '<h1>Hello from Lambda</h1>')

    # Call the PDF generation API
    payload = json.dumps({
        'html': html,
        'engine': 'quality',
        'format': 'A4',
    }).encode('utf-8')

    req = urllib.request.Request(
        'https://api.pdf.funbrew.cloud/v1/pdf/from-html',
        data=payload,
        headers={
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json',
        },
        method='POST',
    )

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            pdf_bytes = resp.read()
    except urllib.error.HTTPError as e:
        error_body = e.read().decode('utf-8')
        raise Exception(f'PDF API error: {e.code} - {error_body}')

    # Upload to S3
    bucket = os.environ['PDF_BUCKET']
    key = f'pdfs/{int(time.time() * 1000)}.pdf'

    s3_client.put_object(
        Bucket=bucket,
        Key=key,
        Body=pdf_bytes,
        ContentType='application/pdf',
    )

    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': 'PDF generated and uploaded',
            's3Key': key,
            'size': len(pdf_bytes),
        }),
    }

With Python, you only need the standard library urllib -- no external packages required. Since boto3 is included in the Lambda runtime, there is no need for a Lambda Layer.

API Gateway + Lambda Architecture

To expose PDF generation as a REST API, combine API Gateway with Lambda.

Architecture

Client --> API Gateway --> Lambda --> FUNBREW PDF API
                             |
                            S3 (PDF storage)
                             |
                          CloudFront (delivery)

SAM Template

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 60
    MemorySize: 256
    Runtime: nodejs20.x
    Environment:
      Variables:
        PDF_BUCKET: !Ref PdfBucket

Resources:
  PdfGeneratorFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      CodeUri: src/
      Events:
        GeneratePdf:
          Type: Api
          Properties:
            Path: /generate-pdf
            Method: post
      Policies:
        - S3CrudPolicy:
            BucketName: !Ref PdfBucket
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:funbrew-pdf-api-key-*

  PdfBucket:
    Type: AWS::S3::Bucket
    Properties:
      LifecycleConfiguration:
        Rules:
          - Id: DeleteOldPdfs
            Status: Enabled
            ExpirationInDays: 30

Outputs:
  ApiEndpoint:
    Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/generate-pdf

Cold Start Optimization

Here are techniques to minimize Lambda cold starts.

1. Provisioned Concurrency

For frequently invoked functions, use Provisioned Concurrency to eliminate cold starts entirely.

# Add to your SAM template
PdfGeneratorFunction:
  Properties:
    ProvisionedConcurrencyConfig:
      ProvisionedConcurrentExecutions: 5

2. Initialize Outside the Handler

AWS SDK clients and cached API keys should be initialized outside the handler function. In the code examples above, the cachedApiKey variable implements this pattern. When a Lambda execution environment is reused, these variables persist.

3. Lightweight Deployment Packages

This is the biggest advantage of using a PDF API. Since you don't need to bundle Chromium, your package size stays at a few KB to a few MB.

# Node.js: Install only the specific AWS SDK v3 packages you need
npm install @aws-sdk/client-s3 @aws-sdk/client-secrets-manager

# Python: No additional packages needed (boto3 is included in the Lambda runtime)

4. SnapStart (Java)

If you use Java Lambdas, enabling SnapStart significantly reduces cold start times.

Timeout Strategies

Lambda has a maximum timeout of 15 minutes, but API Gateway imposes a 29-second limit.

Synchronous Processing

A single PDF generation typically completes in 1-5 seconds, so API Gateway's timeout is not an issue.

// Set Lambda timeout to 60 seconds (with headroom)
// API Gateway timeout is 29 seconds

When You Need Async Processing

For bulk PDF generation or complex templates, use an asynchronous pattern.

Client --> API Gateway --> Lambda (enqueue job)
                             |
                           SQS / EventBridge
                             |
                           Lambda (generate PDF) --> S3
                             |
                           Webhook notification --> Client
const { SQSClient, SendMessageCommand } = require('@aws-sdk/client-sqs');
const sqs = new SQSClient({ region: 'ap-northeast-1' });

// Job enqueue Lambda
exports.enqueueHandler = async (event) => {
  const body = JSON.parse(event.body);
  const jobId = `job-${Date.now()}`;

  await sqs.send(new SendMessageCommand({
    QueueUrl: process.env.PDF_QUEUE_URL,
    MessageBody: JSON.stringify({
      jobId,
      html: body.html,
      webhookUrl: body.webhookUrl,
    }),
  }));

  return {
    statusCode: 202,
    body: JSON.stringify({ jobId, status: 'queued' }),
  };
};

// PDF generation Lambda (SQS trigger)
exports.processHandler = async (event) => {
  for (const record of event.Records) {
    const { jobId, html, webhookUrl } = JSON.parse(record.body);

    // Generate PDF
    const pdfBuffer = await generatePdf(html);

    // Upload to S3
    const s3Key = `pdfs/${jobId}.pdf`;
    await uploadToS3(s3Key, pdfBuffer);

    // Send webhook notification
    if (webhookUrl) {
      await fetch(webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          event: 'pdf.generated',
          jobId,
          s3Key,
          size: pdfBuffer.length,
        }),
      });
    }
  }
};

For detailed webhook implementation, see the webhook integration guide.

S3 Integration: Managing Generated PDFs

Presigned URLs for Delivery

To deliver generated PDFs to users, use S3 presigned URLs to create authenticated temporary download links.

const { GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

async function getDownloadUrl(s3Key) {
  const command = new GetObjectCommand({
    Bucket: process.env.PDF_BUCKET,
    Key: s3Key,
  });

  // Presigned URL valid for 1 hour
  return await getSignedUrl(s3, command, { expiresIn: 3600 });
}

Lifecycle Rules

Set up lifecycle rules to automatically delete old PDFs. The SAM template example above deletes PDFs after 30 days.

Troubleshooting

Common Errors and Solutions

Error Cause Solution
Task timed out Lambda/API Gateway timeout exceeded Adjust timeout values or switch to async processing
ECONNRESET Network error during API call Implement retry logic
AccessDeniedException Insufficient IAM permissions Check S3 and Secrets Manager policies
413 Payload Too Large HTML payload too large Optimize HTML, use external CSS/images

For detailed error handling patterns, see the error handling guide.

Retry Strategy

Build retry logic into your Lambda function.

async function callPdfApiWithRetry(html, maxRetries = 3) {
  const apiKey = await getApiKey();

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ html, engine: 'quality', format: 'A4' }),
        signal: AbortSignal.timeout(30000),
      });

      if (response.ok) {
        return Buffer.from(await response.arrayBuffer());
      }

      // Don't retry on 4xx client errors
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error: ${response.status}`);
      }
    } catch (error) {
      if (attempt === maxRetries) throw error;
      // Exponential backoff
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
    }
  }
}

Debugging with CloudWatch Logs

Output structured logs for easy searching in CloudWatch Logs Insights.

console.log(JSON.stringify({
  level: 'info',
  event: 'pdf_generation',
  status: 'success',
  duration_ms: endTime - startTime,
  pdf_size: pdfBuffer.length,
  s3_key: key,
}));

Production Checklist

  • API keys stored in Secrets Manager or encrypted environment variables
  • Lambda timeout set appropriately (60 seconds recommended)
  • Retry logic implemented
  • S3 bucket lifecycle rules configured
  • CloudWatch Alarms monitoring error rates
  • DLQ (Dead Letter Queue) configured

For more production configuration details, see the production guide.

Summary

AWS Lambda combined with a PDF API is the optimal approach for serverless PDF generation. No need to bundle Chromium -- your deployment packages stay lightweight, cold starts are fast, and memory usage is minimal.

  • Synchronous: API Gateway + Lambda for simple PDF generation
  • Asynchronous: SQS + Lambda + Webhooks for bulk PDF generation
  • Delivery: S3 + CloudFront + presigned URLs for secure distribution

Get started by creating a free account and testing the API in the Playground.

Related

Powered by FUNBREW PDF