How to Make a PDF from HTML with Node.js and Puppeteer
Use Puppeteer’s page.pdf() to turn a URL or HTML string into a PDF. Configure page size, print styles, backgrounds, and reliable rendering waits.

The direct answer: use Puppeteer’s page.pdf() method. Launch Puppeteer, load a URL with page.goto() or provide markup with page.setContent(), wait until the page is ready, and call page.pdf() with the PDF settings you need. By default, PDF output uses print CSS, omits background graphics, and uses Letter paper unless you choose another format. Puppeteer’s PDF guide documents the basic flow and its print behavior.
1. Install Puppeteer
Start with a Node.js project and install Puppeteer:
npm init -y
npm install puppeteer
Puppeteer installs a compatible browser for its default setup. Its documentation guarantees compatibility with the bundled browser; using a different browser binary is at your own risk. For a server or container deployment, make sure the process can start that browser and has enough memory and disk space for the pages you render. See the official LaunchOptions reference for launch configuration.
The examples below use ES modules. Add "type": "module" to package.json, or save the files with an .mjs extension. If your project uses CommonJS, see the short adaptation later in this guide.
2. Create a PDF from a URL
This runnable script opens a URL, waits for a navigation condition, creates an A4 PDF with background graphics, and closes the browser even if navigation or printing fails.

import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://example.com';
const outputPath = process.argv[3] ?? 'output.pdf';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30_000,
});
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
});
console.log(`Saved ${outputPath}`);
} finally {
await browser.close();
}
Run it with:
node make-pdf.js https://example.com report.pdf
The official guide uses waitUntil: 'networkidle2' in its example. Treat that as one possible readiness condition, not a guarantee that every application has finished rendering. A client-side app can still be loading data or replacing content after the network becomes idle. For those pages, wait for a meaningful selector or an application-specific ready signal before printing.
3. Create a PDF from an HTML string
When your HTML is already in memory—for example, a rendered invoice template—use page.setContent() instead of navigating to a URL. The Page.setContent() reference describes how to assign markup to a page.
import puppeteer from 'puppeteer';
const html = `
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font: 16px Arial, sans-serif; margin: 32px; }
h1 { color: #17324d; }
</style>
</head>
<body>
<h1>Invoice</h1>
<p>Rendered from an HTML string.</p>
</body>
</html>
`;
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'load' });
await page.pdf({ path: 'invoice.pdf', format: 'A4', printBackground: true });
} finally {
await browser.close();
}
If the markup references external images, stylesheets, or fonts, those resources still need to load. For generated documents, embedding styles and using dependable asset URLs can make output more predictable. If the HTML includes user-supplied content, escape it appropriately before inserting it into a template.
4. Wait for the right content before printing
PDF generation captures the page’s current rendered state. A navigation event only tells you about a browser loading milestone; it may not mean your application has finished fetching data, drawing a chart, or inserting a late-loading component. Choose a wait that reflects what the page must contain.
Wait for an application selector
If the page exposes a stable element when its content is ready, wait for it explicitly:
await page.goto('https://example.com/report', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-report-ready="true"]', { timeout: 15_000 });
await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });
The selector should represent the content you intend to print, not just a generic page shell. If the application can show an error state, check that state as well so the script does not silently save an error page as a successful report.
Wait for a page function or a deliberate delay
For an application you control, expose a reliable readiness flag and wait for it:
await page.waitForFunction(() => window.reportReady === true, { timeout: 15_000 });
A fixed delay can help with a known short animation or delayed widget, but it is less reliable: it may waste time on fast pages and still be too short on slow ones. Prefer a signal tied to the actual content. Puppeteer’s PDF method waits for fonts by default; the PDFOptions reference documents waitForFonts and other PDF settings.
5. Choose print CSS, page size, and margins
page.pdf() renders with the CSS print media type. This means rules inside @media print apply by default, and screen-specific layout may not carry over as expected. To produce output styled for screen media instead, call page.emulateMediaType('screen') before generating the PDF. The Page.pdf() API documents this behavior.

await page.emulateMediaType('screen');
await page.pdf({ path: 'screen-styled.pdf', format: 'A4', printBackground: true });
For documents intended to print, add print-specific rules to your page. CSS can set paper dimensions and page breaks:
<style>
@page { size: A4; margin: 18mm; }
@media print {
.screen-only { display: none; }
.chapter { break-before: page; }
h1, h2 { break-after: avoid; }
body { -webkit-print-color-adjust: exact; }
}
</style>
The -webkit-print-color-adjust property can be used when colors need to print as specified rather than being adjusted by the browser. Confirm the result against your intended document design, particularly for colored backgrounds and low-contrast text.
When choosing dimensions, decide whether the CSS @page rule or Puppeteer options should control the paper size. With preferCSSPageSize: true, CSS page size takes priority over width, height, or format. Otherwise, the PDF options can determine the size. The default format is Letter, landscape is false, scale is 1, and margins are unset.
| Option | What it controls | When to set it |
|---|---|---|
format |
Named paper size, such as A4 or Letter |
Use a standard page size when CSS does not need to define it. |
width / height |
Explicit page dimensions | Use for a custom document size; check units and CSS page rules. |
landscape |
Horizontal page orientation | Set true for wide tables or diagrams. |
margin |
Top, right, bottom, and left page margins | Set explicit margins when layout needs consistent printable space. |
preferCSSPageSize |
Whether CSS @page size takes priority |
Set true when the stylesheet owns paper dimensions. |
scale |
Scale factor for printed content | Adjust only when needed to fit a layout; changing it affects text and spacing. |
6. Include backgrounds and choose output handling
Background colors and images are omitted by default. Set printBackground: true when the PDF should include them. This can increase output size, especially on image-heavy pages, so turn it on only when those graphics matter.
When path is set, Puppeteer writes the PDF to that file. Without a path, the method returns a Uint8Array, which your application can store, stream, or pass to another library. For example, to get bytes:
const pdfBytes = await page.pdf({ format: 'A4', printBackground: true });
// Pass pdfBytes to your storage or response layer.
For an HTTP endpoint, avoid buffering arbitrarily large documents in memory without limits. Set request and execution timeouts, cap input sizes, and define what the service does if PDF generation fails. The exact streaming or storage integration depends on your server framework and deployment.
7. Complete CommonJS and server patterns
If the project uses CommonJS, load Puppeteer with require and wrap top-level await in an async function:
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.pdf({ path: 'output.pdf', format: 'A4', printBackground: true });
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
For a service that generates many PDFs, consider reusing a browser process and creating a fresh page for each job, then closing each page when finished. Always close resources in a finally block. A crashed or abandoned browser can hold memory and leave jobs waiting. If jobs run concurrently, bound the number of active pages according to the memory available to the service.
8. Or skip the browser setup
If you need a screenshot or PDF from a page and do not want to maintain a browser process, ScreenshotNeo offers a website screenshot API and MCP server. For a one-call screenshot, use the documented request shape below; see the ScreenshotNeo API docs for the PDF request and available options.
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. The API returns screenshots or PDFs and supports options such as full-page capture, custom viewport, wait conditions, and caching. Get 1,000 free screenshots a month with no card.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The PDF is blank or missing app content | The script printed before client-side rendering or data loading completed. | Wait for an application-specific selector or readiness flag, and check for an error state before printing. |
| Background colors or images are missing | printBackground defaults to false. |
Set printBackground: true. Also check that the assets loaded and that print CSS does not hide them. |
| Layout differs from the browser view | PDF generation uses print CSS by default, or print styles change dimensions and visibility. | Review @media print and @page rules. Call emulateMediaType('screen') if screen styling is the intended output. |
| Wrong paper size or clipped content | CSS page dimensions and PDF options may conflict, or the content exceeds the printable area. | Choose whether CSS or options control size. Set preferCSSPageSize accordingly, then review margins, orientation, and page breaks. |
| Fonts look different or some glyphs are missing | The font may not have loaded or may not include the required characters. | Wait for font readiness where appropriate, confirm the font request succeeds, and use a font with the needed glyphs. Puppeteer waits for document.fonts.ready by default. |
| Navigation times out | The site is slow, keeps connections open, or the chosen wait condition never occurs. | Pick a suitable navigation milestone, set a deliberate timeout, and wait separately for the content you need. Avoid treating network idle as a universal readiness signal. |
| Browser fails to launch in deployment | The environment lacks required browser dependencies or uses an unsupported external browser binary. | Use Puppeteer’s bundled browser setup and follow the official deployment guidance for your environment. Verify launch configuration and available resources. |
| The process uses too much memory | Many pages or large documents are rendering concurrently, or browser resources are not closed. | Close pages and browsers in cleanup paths, limit concurrent jobs, and avoid loading assets the PDF does not need. |
| PDF file is missing or cannot be opened | The destination directory may not exist or be writable, or the process may have failed before completion. | Check the output path and permissions, await page.pdf(), and surface errors to the calling code. |
10. Performance, reliability, and cost
PDF generation consumes browser and page resources. Reusing a browser can avoid repeated launches in a long-running service, while creating a separate page for each job keeps page state isolated. Set a concurrency limit: wide pages, long documents, large images, and embedded fonts can increase memory use and render time. Remove unneeded assets and avoid waiting on unrelated background requests when your application has a more precise ready signal.
Reliability starts with explicit cleanup and observable outcomes. Use try/finally for browser shutdown, handle navigation and PDF errors, and distinguish a completed document from a failed or timed-out job. If output is stored or served elsewhere, verify that the PDF bytes were actually persisted or sent. Do not rely on a navigation milestone alone when the page continues rendering afterward.
Puppeteer is an open-source browser automation library, but running it has infrastructure costs: compute time, memory, storage, and the engineering work of maintaining a compatible runtime. The research sources do not establish a universal price per PDF or render-time benchmark, so estimate cost using your own page mix and hosting environment. The documented PDF option timeout default is 30,000 milliseconds; treat it as an API default, not a recommended service-wide job limit.
11. Frequently asked questions
Can Puppeteer make a PDF without saving it to disk?
Yes. Omit path in page.pdf(); Puppeteer returns a Uint8Array that your program can pass to another layer.
Does Puppeteer use screen CSS when making a PDF?
It uses print media by default. Call page.emulateMediaType('screen') before page.pdf() when you want screen media styles.
Why does networkidle2 still produce an incomplete document?
Network activity becoming idle does not prove that application-specific rendering or delayed content has finished. Wait for a meaningful element or readiness signal from the page.
Can I control page breaks?
Yes. Use print CSS such as break-before and break-after, and check the resulting pagination with the page’s actual content and selected paper size.
Will output be identical with any Chrome installation?
Puppeteer guarantees compatibility with its bundled browser. Its documentation does not guarantee identical behavior with arbitrary external browser binaries.


