Puppeteer PDF: Common Problems and How to Fix Them
Your Puppeteer PDF script works perfectly on your laptop. Then you deploy to production and everything falls apart. Timeouts, memory errors, blank pages, missing fonts. This guide covers the most common Puppeteer PDF issues and how to fix them with working code examples.
#Timeouts and how to handle them
Timeouts are the most frequent Puppeteer issue. Your script works locally with fast internet and a powerful machine, but fails in production where conditions are different.
#Why timeouts happen
- Network latency: External resources (fonts, images, scripts) take longer to load
- Heavy JavaScript: Single-page apps need time to render
- Large pages: Complex DOM takes longer to paint
- Slow servers: The target URL responds slowly
#Choosing the right waitUntil option
Puppeteer's waitUntil option controls when the page is considered "loaded":
// Fires when HTML is parsed (fastest, but content may not be ready)
await page.goto(url, { waitUntil: 'domcontentloaded' });
// Fires when all resources are loaded (images, stylesheets)
await page.goto(url, { waitUntil: 'load' });
// Fires when no more than 2 network connections for 500ms
await page.goto(url, { waitUntil: 'networkidle2' });
// Fires when no network connections for 500ms (slowest, most reliable)
await page.goto(url, { waitUntil: 'networkidle0' });
For most PDF generation, networkidle2 is a good balance. Use networkidle0 for pages with lazy-loaded content.
#Robust timeout handling
Set appropriate timeouts and handle failures gracefully:
const puppeteer = require('puppeteer');
async function generatePdf(url, options = {}) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Set default timeout for all operations
page.setDefaultTimeout(options.timeout || 30000);
try {
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: options.navigationTimeout || 30000,
});
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
timeout: options.pdfTimeout || 30000,
});
return pdf;
} catch (error) {
if (error.name === 'TimeoutError') {
console.error(`Timeout loading ${url}`);
// Retry logic or fallback here
}
throw error;
} finally {
await browser.close();
}
}
Increase timeouts for complex pages
If you're converting pages with heavy JavaScript frameworks (React, Vue, Angular), start with 60 second timeouts and adjust based on your actual performance data.
#Memory leaks and high RAM usage
Memory issues are the second most common Puppeteer problem. Chrome is memory-hungry, and mistakes in your code can make it worse.
#The most common cause: not closing resources
Every browser instance you launch consumes 150-300MB. Every page you open adds more. If you don't close them, memory grows until your server crashes.
// BAD: Browser and pages never closed
async function generatePdf(html) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html);
const pdf = await page.pdf({ format: 'A4' });
return pdf;
// Browser stays open forever!
}
// GOOD: Always close browser in finally block
async function generatePdf(html) {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4' });
return pdf;
} finally {
await browser.close();
}
}
#Browser pool pattern for concurrent requests
For high-throughput PDF generation, don't launch a new browser for each request. Reuse a single browser instance:
const puppeteer = require('puppeteer');
class PdfGenerator {
constructor() {
this.browser = null;
}
async init() {
this.browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
}
async generatePdf(html) {
if (!this.browser) {
await this.init();
}
const page = await this.browser.newPage();
try {
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
});
return pdf;
} finally {
await page.close(); // Close page, not browser
}
}
async shutdown() {
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
}
// Usage
const generator = new PdfGenerator();
await generator.init();
// Generate multiple PDFs with same browser
const pdf1 = await generator.generatePdf('<html>...</html>');
const pdf2 = await generator.generatePdf('<html>...</html>');
// Shutdown when done
await generator.shutdown();
Memory per instance
Each Chrome instance consumes 150-300MB of RAM. If you're running multiple browser instances for concurrency, memory usage adds up fast. Monitor your server's memory and set appropriate limits.
#Docker and container issues
Running Puppeteer in Docker is notoriously tricky. The default Node.js images are missing Chrome's dependencies.
#Missing Chromium dependencies
When you see errors like error while loading shared libraries or cannot open shared object file, Chrome's system dependencies are missing.
#Complete Dockerfile
Here's a Dockerfile that works:
FROM node:20-slim
# Install Chrome dependencies
RUN apt-get update && apt-get install -y \
chromium \
fonts-liberation \
fonts-noto-color-emoji \
libatk-bridge2.0-0 \
libatk1.0-0 \
libcups2 \
libdrm2 \
libgbm1 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
xdg-utils \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Set Puppeteer to use system Chrome
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER node
CMD ["node", "server.js"]
#The --no-sandbox flag
In containers, Chrome often needs --no-sandbox to run. This disables Chrome's sandbox security feature.
const browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage', // Overcome limited /dev/shm in Docker
'--disable-gpu',
],
});
Security consideration
--no-sandbox reduces Chrome's security isolation. This is acceptable in containers where Chrome is already isolated, but don't use it on shared systems where untrusted content might be rendered.
#Use puppeteer-core for smaller images
Instead of installing Puppeteer's bundled Chromium, use puppeteer-core with the system Chrome:
npm install puppeteer-core
const puppeteer = require('puppeteer-core');
const browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
This can reduce your Docker image size significantly since you're not downloading Chromium twice.
#Font rendering problems
Fonts are a common source of PDF rendering issues. The fonts installed on your development machine aren't available on your server.
#The problem
Your PDF looks great locally with Helvetica, but on the server, Chrome falls back to a different font (often DejaVu or Liberation Sans), and your layout breaks.
#Solution 1: Install fonts on the server
For the Dockerfile above, we installed fonts-liberation and fonts-noto-color-emoji. Add more font packages as needed:
RUN apt-get update && apt-get install -y \
fonts-liberation \
fonts-noto-cjk \
fonts-noto-color-emoji \
fonts-roboto \
--no-install-recommends
#Solution 2: Use Google Fonts with proper wait
Google Fonts work well, but you need to wait for them to load:
const html = `
<html>
<head>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
`;
await page.setContent(html, { waitUntil: 'networkidle0' });
// Extra wait to ensure fonts are applied
await page.evaluateHandle('document.fonts.ready');
const pdf = await page.pdf({ format: 'A4' });
#Solution 3: Embed fonts as base64
For guaranteed font rendering, embed fonts directly in your CSS:
const fs = require('fs');
// Read font file and convert to base64
const fontBuffer = fs.readFileSync('fonts/Inter-Regular.woff2');
const fontBase64 = fontBuffer.toString('base64');
const html = `
<html>
<head>
<style>
@font-face {
font-family: 'Inter';
src: url(data:font/woff2;base64,${fontBase64}) format('woff2');
font-weight: 400;
}
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
`;
Test font loading
Before generating the PDF, you can check which fonts are available: await page.evaluate(() => document.fonts.check('16px Inter')) returns true if the font is loaded.
#Blank or incomplete PDFs
You generate a PDF and it's blank, or it's missing content that appears when you view the HTML in a browser.
#The problem
Your content loads asynchronously after page.setContent returns. JavaScript frameworks like React, Vue, or Angular often render content after the initial page load.
#Wait for specific elements
Use waitForSelector to wait for content to appear:
await page.setContent(html, { waitUntil: 'domcontentloaded' });
// Wait for a specific element that indicates content is ready
await page.waitForSelector('.invoice-ready', { timeout: 10000 });
const pdf = await page.pdf({ format: 'A4' });
#Custom wait for SPAs
For single-page apps, wait for the app to signal readiness:
// In your HTML/React app, set a flag when ready
// window.__PDF_READY__ = true;
await page.setContent(html, { waitUntil: 'networkidle0' });
// Wait for the app to set the ready flag
await page.waitForFunction(() => window.__PDF_READY__ === true, {
timeout: 30000,
});
const pdf = await page.pdf({ format: 'A4' });
#Wait for images to load
If images are important, wait for them explicitly:
await page.setContent(html, { waitUntil: 'networkidle0' });
// Wait for all images to load
await page.evaluate(async () => {
const images = Array.from(document.querySelectorAll('img'));
await Promise.all(
images.map((img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
})
);
});
const pdf = await page.pdf({ format: 'A4' });
#Large PDF file sizes
Your PDF is megabytes when it should be kilobytes. This affects storage costs and download times.
#printBackground bloats file size
printBackground: true is often necessary for styled documents, but it increases file size because Chrome renders backgrounds as images rather than vectors.
// Only use printBackground if you actually need backgrounds
const pdf = await page.pdf({
format: 'A4',
printBackground: true, // Remove this if you don't need backgrounds
});
#Optimize images before conversion
Large images embedded in your HTML are the biggest file size culprit. Optimize them before including in your HTML:
- Resize images to the actual display size (don't use a 4000px image for a 400px display)
- Compress images (JPEG quality 80% is usually fine for documents)
- Use appropriate formats (JPEG for photos, PNG for graphics with transparency)
#Use scale for smaller files
The scale option affects rendering quality and file size:
const pdf = await page.pdf({
format: 'A4',
scale: 0.8, // 80% scale, smaller file, slightly lower quality
});
Trade-off
Lower scale means smaller files but reduced quality. For documents with small text, keep scale at 1 or higher. For image-heavy content, lower scale is often acceptable.
#When to consider alternatives
Puppeteer is powerful but comes with operational overhead. Consider alternatives when:
- Memory is constrained: Serverless platforms (Vercel, Netlify Functions, AWS Lambda) have memory limits and cold start issues that make running Chrome difficult
- Scale is unpredictable: Managing Chrome instances for variable load requires infrastructure work
- You're spending more time on Chrome than your actual product: There's a point where the ops burden isn't worth it
#Options
- PDF APIs: Services like PDFLoom, DocRaptor, or Prince handle the browser infrastructure for you. You send HTML, get a PDF back.
- wkhtmltopdf: Lighter weight but uses an older rendering engine with limited CSS support
- Chrome in the cloud: Services like Browserless give you a managed Chrome instance
For a deeper comparison of these approaches, see our guide on How to Generate PDFs from HTML in Node.js.
#Next steps
Puppeteer is a capable tool once you understand its quirks. The most common issues are timeouts, memory management, and Docker configuration. With the patterns in this guide, you should be able to debug most problems.
If you'd rather skip the infrastructure complexity, you can create a free PDFLoom account and get 50 credits to test PDF generation without managing browsers. Check the API documentation for the full list of options.
Related posts
PDF Generation in Serverless Environments
Learn how to generate PDFs in serverless environments like Vercel, AWS Lambda, and Netlify. Includes working code examples and a comparison of approaches.
Convert Any URL to PDF with a Single API Call
Learn how to convert any public URL to a PDF or screenshot using the PDFLoom API. Includes code examples in cURL, Node.js, and a full Express.js integration.
Generate Invoice PDFs from HTML Templates
Learn how to create professional invoice PDFs using HTML templates and a PDF API. Includes a complete invoice template and Node.js code examples.