Puppeteer PDF Timeout on Vercel: Why It Happens
Vercel's serverless functions have strict timeout limits: 10 seconds on the Hobby plan and 60 seconds on Pro. Chrome startup alone takes 2-5 seconds on cold starts, and rendering a complex PDF adds 3-10 seconds, frequently exceeding the 10-second limit. Upgrading to Pro delays the problem but doesn't solve it—complex documents or concurrent renders still hit the 60-second wall. The root issue is that Vercel's ephemeral execution model conflicts with Chrome's heavy initialization.
Why This Happens
Vercel deploys serverless functions as AWS Lambda functions behind the scenes. Each invocation has:
- Hobby plan timeout: 10 seconds maximum execution time
- Pro plan timeout: 60 seconds maximum execution time
- Cold start penalty: First invocation after idle period requires full environment initialization
- Chrome binary size: 150-300MB must be loaded into memory
- Process startup: Chrome's multi-process architecture takes time to launch
Here's the typical breakdown for a Puppeteer PDF render:
| Operation | Time (Cold Start) | Time (Warm) |
|---|---|---|
| Function init | 500-1000ms | 50-100ms |
| Chrome launch | 2000-4000ms | 500-1000ms |
| Page load + setContent | 500-2000ms | 500-2000ms |
| PDF generation | 1000-5000ms | 1000-5000ms |
| Total | 4-12 seconds | 2-7 seconds |
On Hobby, cold starts almost always timeout. Even warm starts can exceed 10 seconds for complex documents.
The Cold Start Problem
Vercel's serverless functions are ephemeral—they spin down after inactivity and must reinitialize on the next request:
What Happens on Cold Start
- Container provisioning: AWS Lambda allocates a new execution environment.
- Deployment package extraction: Your function code and dependencies (including Chrome binary) are extracted from S3.
- Node.js startup: The runtime initializes (usually fast, 100-500ms).
- Chrome binary load: The 150-300MB Chromium binary must be read from disk into memory (2-4 seconds).
- Browser launch: Puppeteer spawns Chrome with multiple processes (DevTools protocol, renderer, GPU process).
Even with Lambda Layers (to cache the Chrome binary), cold starts add 2-5 seconds before your code runs.
Warm vs Cold Start Frequency
- Warm: If a request arrives within 5-15 minutes of the last one, the container is reused.
- Cold: After idle period, or when concurrent requests exceed warm containers, new containers must start.
At low traffic (<1 request/minute), most invocations are cold. At high traffic, concurrent requests force cold starts even if some containers are warm.
Why Complex PDFs Make It Worse
Certain document characteristics increase render time:
- Large HTML documents: 100KB+ HTML takes longer to parse and layout (1-3 seconds).
- Many images: External images must be fetched and decoded (500ms-2s per image if remote).
- Custom web fonts:
@font-facedownloads and font rendering add 500ms-1s. - Heavy JavaScript: If your HTML includes client-side JS, execution time adds up.
- High page count: Multi-page documents with complex layouts take longer to paginate (1-2s per page).
A simple invoice might render in 2 seconds warm, but a 20-page report with charts and images can take 8-12 seconds even on a warm container.
Common Workarounds (And Why They Fail)
1. Upgrade to Pro ($20/month for 60s timeout)
Why it helps: Buys you more time—60 seconds instead of 10.
Why it fails:
- Cold starts still consume 4-6 seconds, leaving only 54-56 seconds for rendering.
- Complex documents still timeout (20-page reports, many images).
- Concurrent renders compound the issue—if 5 users hit the endpoint simultaneously, all get cold starts.
- You're paying for a workaround, not a solution.
2. Reduce PDF complexity
Why it helps: Smaller HTML, fewer images, and simpler layouts render faster.
Why it fails:
- Defeats the purpose—users need the complex document.
- Design compromises hurt user experience.
- You're limited in what you can build.
3. Split rendering across multiple functions
Why it helps: Could parallelize work (render pages separately, then merge).
Why it fails:
- Adds massive complexity (orchestration, state management, PDF merging).
- Each function still has cold start penalty.
- Merging PDFs introduces new failure modes (page order, bookmarks, metadata).
- You've built a distributed rendering system—now you're managing infrastructure anyway.
4. Use Vercel's maxDuration config
You can configure per-function timeout limits:
// api/generate-pdf.js
export const config = {
maxDuration: 60, // Pro plan only
}
export default async function handler(req, res) {
// Puppeteer code...
}
Why it fails:
- Only works on Pro plan ($20/month minimum).
- Doesn't reduce cold start time—just gives you more room to timeout later.
- Still fails for complex documents.
Why This Doesn't Happen in Docker
If you deploy Puppeteer in a long-running Docker container (ECS, Kubernetes, EC2):
- No cold starts: Container stays running, Chrome process stays alive between requests.
- Reusable browser: You can launch Chrome once and reuse the same instance (connection pooling).
- No timeout limits: You control execution time limits (though you still hit memory leaks eventually—see Puppeteer PDF Memory Leak).
But now you're managing infrastructure:
- Autoscaling, load balancing, health checks
- Security updates, Docker image builds
- Monitoring, alerting, log aggregation
- Cost: ~$50-200/month for a dedicated service
The Architectural Solution
Instead of deploying Chrome in your Vercel function, delegate rendering to a stateless external service:
Architecture Comparison
Self-hosted (what fails):
User Request → Vercel Function → Launch Chrome → Render PDF → Return PDF
↑ 4-12s cold start ↑ 2-7s render time
Managed API (what works):
User Request → Vercel Function → API Call (JSON) → Return PDF
↑ <100ms ↑ 1-3s consistent
Benefits:
- No Chrome binary in deployment: Your Vercel function is <1MB, starts in <100ms.
- No cold start penalty: Rendering service maintains warm Chrome pool.
- Consistent performance: Sub-second API response time (p95 <2s).
- Scales independently: Rendering service handles concurrency without affecting your function quota.
Code Comparison
Before (Vercel + Puppeteer - timeouts on cold start)
// api/generate-pdf.js
import puppeteer from 'puppeteer-core'
import chromium from '@sparticuz/chromium'
export const config = {
maxDuration: 60, // Pro plan required ($20/month)
}
export default async function handler(req, res) {
const startTime = Date.now()
let browser
try {
// This takes 2-5 seconds on cold start
browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: chromium.headless,
})
console.log(`Browser launch: ${Date.now() - startTime}ms`)
const page = await browser.newPage()
await page.setContent(req.body.html, {
waitUntil: 'networkidle0', // Waits for all resources
timeout: 30000, // Can timeout here too
})
console.log(`Content loaded: ${Date.now() - startTime}ms`)
// PDF generation takes 1-5s for complex docs
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
timeout: 30000,
})
console.log(`PDF generated: ${Date.now() - startTime}ms`)
res.setHeader('Content-Type', 'application/pdf')
res.send(pdf)
} catch (error) {
console.error('Render failed:', error.message)
console.error(`Total time before failure: ${Date.now() - startTime}ms`)
// Common error: "Navigation timeout of 30000 ms exceeded"
// Or: "Function execution duration limit exceeded"
res.status(500).json({ error: error.message })
} finally {
if (browser) {
await browser.close()
}
}
}
// Deployment size: 150-300MB (mostly Chrome binary)
// Cold start: 4-6 seconds
// Warm render: 2-7 seconds
// Fails on: Hobby (10s timeout), complex docs on Pro (60s timeout)
After (Vercel + Rendering API - consistent sub-second function time)
// api/generate-pdf.js
// No Puppeteer, no Chrome binary, no timeout issues
export default async function handler(req, res) {
const { templateId, data } = req.body
try {
const response = await fetch('https://api.hundredocs.com/v1/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.HUNDRED_DOCS_API_KEY,
},
body: JSON.stringify({ templateId, data }),
// API handles Chrome, warm pools, etc.
})
if (!response.ok) {
const error = await response.text()
throw new Error(`Render failed: ${response.status} ${error}`)
}
const pdfBuffer = await response.arrayBuffer()
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', `attachment; filename="document.pdf"`)
res.send(Buffer.from(pdfBuffer))
} catch (error) {
console.error('Render failed:', error.message)
res.status(500).json({ error: error.message })
}
}
// Deployment size: <1MB (no Chrome binary)
// Cold start: <200ms
// Function execution: 300ms-1s (just API call forwarding)
// API response time: 1-3s (p95)
// Works on: Hobby (10s is plenty), Pro (never gets close to 60s)
Performance Comparison
| Metric | Vercel + Puppeteer (Hobby) | Vercel + Puppeteer (Pro) | Vercel + API |
|---|---|---|---|
| Cold start | 4-6s | 4-6s | <200ms |
| Warm render (simple) | 2-3s | 2-3s | 1-2s |
| Warm render (complex) | 8-12s (timeout) | 8-12s | 2-4s |
| Timeout limit | 10s ❌ | 60s ⚠️ | 10s ✅ |
| Deployment size | 150-300MB | 150-300MB | <1MB |
| Monthly cost | $0 (fails) | $20+ | $0 + API cost |
| Memory leaks | Yes (see guide) | Yes | No (stateless) |
When to Use Each Approach
Use Puppeteer on Vercel when:
- Volume is <5 PDFs/day (cold starts acceptable).
- Documents are extremely simple (1-2 seconds to render).
- You're prototyping and okay with failures.
Use Puppeteer on long-running servers (ECS/EC2) when:
- Volume is 10-100 PDFs/day.
- You have DevOps resources to manage infrastructure.
- You need full control over Chrome flags and rendering options.
Use managed rendering APIs when:
- Volume is >50 PDFs/day (see Invoice PDF Generation).
- Deploying on Vercel Hobby or want to stay serverless.
- You want predictable performance without cold start variance.
- Non-developers need to edit templates (visual editor).
Related Guides
- Puppeteer PDF Alternative - Why managed APIs beat self-hosted
- Puppeteer PDF Memory Leak - What happens after you fix timeouts
- Serverless PDF Generation - Architecture patterns
- Bulk PDF Report Generation - Handling high volume
Tools like Hundred Docs maintain warm Chrome pools in dedicated rendering infrastructure, so cold starts don't affect your Vercel function. Your function becomes a lightweight proxy that sends JSON and streams back PDFs, staying well under even the 10-second Hobby limit.