Build or buy, in two paragraphs
Puppeteer is an open-source JavaScript library, under the Apache-2.0 licence, that controls Chrome or Firefox. Calling page.pdf() prints whatever the browser shows. The library is free, and you control every step. You also own every server, every crash and every Chrome upgrade.
CastPDF runs that browser work for you. You send HTML, or a template ID and JSON, and get a PDF back. It renders in Chromium too, adds a print step for long documents, and costs $19 a month for 2,500 PDFs. Under 100 a month it is free.
Prices side by side
The cheapest monthly plan that includes each volume, from each vendor's public pricing page (checked October 2026, before tax). Where a vendor sells extra documents on a smaller plan, that can cost less than the plan shown; the text below works it out where it matters. CastPDF counts one PDF per document, whatever its page count.
| PDFs a month | CastPDF | Puppeteer |
|---|---|---|
| 1,000 | $19 (Starter) | Free software; you run the servers |
| 10,000 | $49 (Growth) | Free software; you run the servers |
| 50,000 | $129 (Pro) | Free software; you run the servers |
| Free plan | 100 PDFs a month | Free software |
What running Puppeteer really costs
The library has no price tag, but the system around it does. When teams add up a self-hosted PDF service, these are the items that tend to appear.
- Servers sized for a browser. Each Chrome process needs a lot of memory, and a busy hour needs several at once. You pay for that capacity all month, even when nobody is printing.
- Memory that creeps up. A page or browser that is not closed after an error keeps its memory. Long-running services need careful cleanup, health checks and regular restarts.
- Chrome updates. Each Puppeteer release is tested against a specific browser version. Upgrades bring security fixes, but also small layout changes you have to catch before customers do.
- Fonts in containers. Slim Docker images ship with few fonts. Missing fonts show up as fallback text or empty boxes, so you install font packages and keep them current.
- Concurrency. Ten invoices at the same second means a queue or a pool of browsers. Without one, requests time out or the process runs out of memory.
- Untrusted HTML. If customers can shape the HTML, the browser can fetch URLs on your network. You need a sandbox and request filtering around it.
None of this is exotic, and plenty of teams run it well. It is simply engineering time that never shows up on an invoice. If one engineer spends two days a quarter on the PDF service, that time often costs more than a year of a small API plan.
Headers, footers and page numbers
Puppeteer handles headers and footers through two options of page.pdf(): headerTemplate and footerTemplate. They only appear with displayHeaderFooter: true. Inside them, elements with the classes pageNumber and totalPages are filled with the numbers.
These templates are separate snippets of HTML, so the styles of your page do not reach them. Most people style them inline and make room for them with the PDF margins. The guide to headers and footers in Puppeteer covers the details.
CastPDF uses the CSS standard for the same job: @page margin boxes in print mode. The header lives in your stylesheet, next to the rest of your design. It can print the current chapter title through string-set, and it can skip the cover with @page :first.
@page {
margin: 22mm 16mm 24mm;
@top-left { content: string(chapter); font-size: 8.5pt; color: #475569; }
@bottom-right { content: "Page " counter(page) " of " counter(pages); font-size: 8.5pt; }
}
@page :first {
@top-left { content: none; }
}
h2 { string-set: chapter content(text); }Long tables and page breaks
Chrome repeats a plain <thead> when a table runs onto a new page, so simple cases work in Puppeteer as well. The harder part is everything around the table: rows split in half, a totals box stranded on its own page, or a block that is quietly cut off.
CastPDF adds rules for that in every document. Table rows, images and anything with the keep-together class avoid page breaks inside them. In print mode the header and its <colgroup> repeat on each continued page. If something would still be cut off, you get a content_lost error instead of a PDF with missing lines. The guide to repeating table headers shows the markup.
What CastPDF costs as you grow
The price table above lists the plan that covers each volume. Between plans, a smaller plan with overage is sometimes cheaper. At 20,000 PDFs a month, Growth plus overage at $6.00 per 1,000 comes to $109. That beats the $129 Pro plan, and it stays inside the default spending cap.
You pay per document, never per page, so a 30-page report costs the same as a one-page receipt. Unused PDFs on paid plans roll over for one month. We email you at 80% and 100% of your allowance, and a spending cap stops overage at a limit you choose. The pricing page has every plan, up to Scale with 200,000 PDFs a month.
When Puppeteer is the better choice
- You already run it, and it is stable. If your service has worked for a year and nobody touches it, a move saves little. Keep it, and revisit when the next Chrome upgrade hurts.
- You need the whole browser. Puppeteer can log in, click, fill forms and wait for a selector before printing. CastPDF takes HTML or a template and prints it; it does not browse to your pages.
- You also need screenshots. Puppeteer captures PNG and JPEG images of pages and elements. CastPDF makes PDFs only.
- You print at very high volume. Millions of documents a month on your own hardware can cost less than any per-document price. CastPDF also limits how many PDFs render at the same time per team: Free 1, Starter 2, Growth 3, Pro 4 and Scale 4.
- No document may leave your network. In an air-gapped system, or under rules that forbid calls to outside services, a library inside your own walls is the only option.
Replacing page.pdf() with one request
Most Puppeteer PDF code follows the same pattern: launch a browser, set the HTML, print, close. Here is a typical version, with a header and a numbered footer.
import puppeteer from 'puppeteer';
import { readFile } from 'node:fs/promises';
const html = await readFile('report.html', 'utf8');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
margin: { top: '25mm', right: '15mm', bottom: '20mm', left: '15mm' },
displayHeaderFooter: true,
headerTemplate: '<div style="font-size:9px;width:100%;text-align:right;margin-right:15mm">Quarterly report</div>',
footerTemplate: '<div style="font-size:9px;width:100%;text-align:center">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
});
} finally {
await browser.close();
}The CastPDF version sends the same HTML. The header and footer move into CSS as margin boxes, and the margins become one string in CSS order: top, sides, bottom.
import { readFile, writeFile } from 'node:fs/promises';
const html = await readFile('report.html', 'utf8');
const css = `
@page {
@top-right { content: "Quarterly report"; font-size: 7pt; }
@bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 7pt; }
}`;
const res = await fetch('https://api.castpdf.com/v1/pdf', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CASTPDF_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'report-2026-q3',
},
body: JSON.stringify({ html, css, mode: 'print', format: 'A4', margins: '25mm 15mm 20mm', filename: 'report.pdf' }),
signal: AbortSignal.timeout(60_000),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
await writeFile('report.pdf', Buffer.from(await res.arrayBuffer()));
console.log('pages:', res.headers.get('x-pages'));| Puppeteer | CastPDF |
|---|---|
page.setContent(html) | The html field (or a stored template with template_id and data) |
format: 'A4', landscape: true | format and orientation |
margin: { top, right, bottom, left } | margins as one string, such as "25mm 15mm 20mm" |
printBackground: true | Always on; nothing to set |
headerTemplate, footerTemplate | @page margin boxes in your CSS, with "mode": "print" |
pageNumber, totalPages classes | counter(page) and counter(pages) |
waitUntil: 'networkidle0' | The renderer waits for fonts and loads images within its limits |
pageRanges | No equivalent: the whole document is rendered |
- Get a free test key. Sign up and create a test key; the quickstart takes about five minutes. Test PDFs are free and watermarked.
- Send your existing HTML. Use the "After" code with your own HTML file. Keep
modeset toprintif you need margin boxes or long tables. - Move the header and footer. Rewrite the two Puppeteer templates as
@pagemargin boxes. Remove their inline sizes and let your CSS style them. - Compare the PDFs. Open both files side by side. Check fonts, the first and last page, and any table that crosses a page.
- Switch to a live key and retire the browser. When the output matches, use a live key in production. Then remove Chrome from your images and delete the browser pool code.
Sources
Every fact about another product on this page comes from that product's own pages, checked October 2026. CastPDF's prices and limits come from our pricing page.
| Puppeteer | What it says | Source |
|---|---|---|
| What it is | A JavaScript library that controls Chrome or Firefox | pptr.dev |
| Licence | Apache-2.0 | github.com/puppeteer/puppeteer |
| Headers and footers | page.pdf() takes headerTemplate and footerTemplate with pageNumber and totalPages classes (needs displayHeaderFooter) | pptr.dev/api/puppeteer.pdfoptions |