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
- FUNBREW PDF API Docs — Endpoint reference and all available options
- Quickstart by Language — Node.js, Python, PHP code examples
- PDF API Error Handling — Retry strategies and exponential backoff
- Webhook Integration Guide — Push notifications on async job completion
- PDF API Production Guide — Security and monitoring for production
- PDF API Security Guide — API key management and access control
- Batch PDF Generation — Throughput optimization for bulk PDF jobs
- Docker and Kubernetes Guide — PDF API in containerized environments
- GitHub Actions CI/CD Guide — PDF generation in deployment pipelines
- Automate Invoice PDFs — Invoice generation implementation patterns
- PDF Template Engine Guide — Dynamic template design
- Use Cases — Common business scenarios for Lambda-based PDF generation