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 averageImplementation: 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.xStep 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 TemplatesPerformance 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.
References & Sources
Technical Implementation Disclaimer
This article provides technical guidance on PDF generation using AWS Lambda. Implementation details, costs, and performance may vary based on specific requirements, AWS region, and usage patterns. Always test thoroughly in development environments and monitor costs in production. AWS pricing is subject to change.
AWS Documentation
PDF Generation Libraries
Serverless Resources
Last updated: October 2025. AWS services and pricing are regularly updated. Consult official AWS documentation for the most current information and best practices.