Back to BlogAWS & Cloud

Automating PDF Document Generation in AWS Lambda: A Step-by-Step Guide for Developers

October 22, 2025
10 min read

Why AWS Lambda for PDF Generation?

  • Cost-effective: Pay only when generating PDFs ($0.0000166667 per GB-second)
  • Scalable: Handle 1 or 10,000 PDFs simultaneously
  • No server management: AWS handles infrastructure
  • Fast: Typical generation time: 800ms-2s per document
  • Integrated: Works seamlessly with S3, DynamoDB, API Gateway

Cost Example

Scenario: Generate 10,000 invoices per month

  • Lambda execution: 1.5s avg @ 1024MB = $2.50/month
  • S3 storage (10,000 PDFs @ 200KB avg): $0.23/month
  • S3 requests: $0.05/month
  • Total: $2.78/month ($0.000278 per PDF)

Architecture Overview

PDF Generation Flow:

1. API Gateway receives request
   ↓
2. Lambda function triggers
   ↓
3. Fetch data from DynamoDB/RDS
   ↓
4. Puppeteer renders HTML template
   ↓
5. Generate PDF in /tmp directory
   ↓
6. Upload to S3 bucket
   ↓
7. Return S3 signed URL (valid 1 hour)
   ↓
8. Optional: Send email via SES with PDF attachment

Total time: 1.2s average

Implementation: Invoice Generator

Step 1: Set Up Lambda Layer with Puppeteer

# Create layer directory
mkdir -p pdf-layer/nodejs/node_modules
cd pdf-layer/nodejs

# Install dependencies
npm install puppeteer-core @sparticuz/chromium

# Create layer zip
cd ..
zip -r pdf-layer.zip .

# Upload to Lambda Layers
aws lambda publish-layer-version \
  --layer-name puppeteer-chromium \
  --zip-file fileb://pdf-layer.zip \
  --compatible-runtimes nodejs18.x nodejs20.x

Step 2: Lambda Function Code

// index.js
const chromium = require('@sparticuz/chromium')
const puppeteer = require('puppeteer-core')
const AWS = require('aws-sdk')
const s3 = new AWS.S3()

exports.handler = async (event) => {
  const { invoiceId, customerId } = JSON.parse(event.body)

  // Launch headless browser
  const browser = await puppeteer.launch({
    args: chromium.args,
    defaultViewport: chromium.defaultViewport,
    executablePath: await chromium.executablePath(),
    headless: chromium.headless,
  })

  const page = await browser.newPage()

  // Load HTML template
  const html = generateInvoiceHTML(invoiceId, customerId)
  await page.setContent(html, { waitUntil: 'networkidle0' })

  // Generate PDF
  const pdf = await page.pdf({
    format: 'A4',
    printBackground: true,
    margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' }
  })

  await browser.close()

  // Upload to S3
  const key = `invoices/${invoiceId}.pdf`
  await s3.putObject({
    Bucket: process.env.PDF_BUCKET,
    Key: key,
    Body: pdf,
    ContentType: 'application/pdf'
  }).promise()

  // Generate signed URL (valid 1 hour)
  const url = s3.getSignedUrl('getObject', {
    Bucket: process.env.PDF_BUCKET,
    Key: key,
    Expires: 3600
  })

  return {
    statusCode: 200,
    body: JSON.stringify({ pdfUrl: url, key })
  }
}

function generateInvoiceHTML(invoiceId, customerId) {
  return `
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        body { font-family: Arial, sans-serif; }
        .header { text-align: center; margin-bottom: 30px; }
        .invoice-details { margin: 20px 0; }
        table { width: 100%; border-collapse: collapse; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
        .total { font-weight: bold; font-size: 18px; }
      </style>
    </head>
    <body>
      <div class="header">
        <h1>INVOICE</h1>
        <p>Invoice #${invoiceId}</p>
      </div>
      <div class="invoice-details">
        <p><strong>Customer ID:</strong> ${customerId}</p>
        <p><strong>Date:</strong> ${new Date().toLocaleDateString()}</p>
      </div>
      <table>
        <thead>
          <tr>
            <th>Item</th>
            <th>Quantity</th>
            <th>Price</th>
            <th>Total</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Professional Plan</td>
            <td>1</td>
            <td>$49.00</td>
            <td>$49.00</td>
          </tr>
        </tbody>
      </table>
      <p class="total">Total: $49.00</p>
    </body>
    </html>
  `
}

Step 3: Configure Lambda Function

# Function configuration
Memory: 1024 MB (Puppeteer needs memory for Chrome)
Timeout: 30 seconds
Environment Variables:
  - PDF_BUCKET: your-pdf-bucket-name

# Attach IAM role with policies:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::your-pdf-bucket-name/*"
    }
  ]
}

Advanced Features

1. Dynamic Data from Database

const dynamodb = new AWS.DynamoDB.DocumentClient()

// Fetch invoice data
const invoice = await dynamodb.get({
  TableName: 'Invoices',
  Key: { invoiceId }
}).promise()

const customer = await dynamodb.get({
  TableName: 'Customers',
  Key: { customerId: invoice.Item.customerId }
}).promise()

// Use real data in template
const html = `
  <h2>${customer.Item.companyName}</h2>
  <p>${customer.Item.address}</p>
  ...
`

2. Custom Fonts and Branding

<style>
  @font-face {
    font-family: 'CustomFont';
    src: url('https://your-cdn.com/fonts/custom.woff2');
  }
  body { font-family: 'CustomFont', sans-serif; }

  .logo {
    width: 200px;
    background: url('https://your-cdn.com/logo.png');
  }
</style>

3. Email PDF Attachments via SES

const ses = new AWS.SES()

await ses.sendRawEmail({
  RawMessage: {
    Data: createMimeEmail({
      to: customer.email,
      subject: `Invoice #${invoiceId}`,
      text: 'Please find your invoice attached.',
      attachments: [{
        filename: `invoice-${invoiceId}.pdf`,
        content: pdf,
        contentType: 'application/pdf'
      }]
    })
  }
}).promise()

Serverless Document Automation

SnapIT Software offers pre-built serverless document generation templates for invoices, reports, certificates, and more. Deploy in minutes with our AWS CDK infrastructure.

Explore Templates

Performance Optimization

1. Warm Starts with Provisioned Concurrency

For high-traffic scenarios, keep Lambda warm:

aws lambda put-provisioned-concurrency-config \
  --function-name pdf-generator \
  --provisioned-concurrent-executions 2

# Reduces cold start from 8s to 0s
# Cost: $10/month for 2 instances (worth it for user-facing PDFs)

2. Caching Templates in S3

// Load template once, reuse across invocations
let cachedTemplate = null

if (!cachedTemplate) {
  const template = await s3.getObject({
    Bucket: 'templates',
    Key: 'invoice-template.html'
  }).promise()

  cachedTemplate = template.Body.toString('utf-8')
}

// Populate template with data
const html = cachedTemplate
  .replace('{{invoiceId}}', invoiceId)
  .replace('{{customerName}}', customer.name)

Common Pitfalls

  • /tmp storage limits: Max 512MB in /tmp (clean up after generating)
  • Memory allocation: Puppeteer needs 1024MB minimum, 1536MB recommended
  • Timeout configuration: Set to 30s (complex PDFs can take 10-15s)
  • Large PDFs: Files >6MB should stream to S3, not return inline
  • Fonts: Self-host fonts or use web fonts (don't rely on system fonts)

Conclusion

AWS Lambda PDF generation is cost-effective, scalable, and surprisingly simple. For $0.0003 per document, you can generate professional invoices, reports, and certificates without managing servers. The serverless model scales automatically from 1 to 10,000 PDFs per minute.

Start with the basic Puppeteer example above, then add database integration, custom branding, and email delivery as needed. Deploy the function, test with a few documents, then scale to production—AWS handles the rest.