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 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.