The one-line difference
Puppeteer (and Playwright, and chrome --headless) drive a real Chromium to render an HTML page and then ask the browser to print it to PDF. makesPDF is a hosted REST API with its own deterministic layout engine: you POST markdown or a compact DSL string and get back a PDF that's PDF/A-2A archival + PDF/UA-1 accessible by construction. No Chromium in your container, no fonts to install, no Xvfb, no flaky CI.
Feature matrix
| Puppeteer / headless Chrome | makesPDF | |
|---|---|---|
| Shape | Library + a 200MB Chromium binary | Hosted REST API (/api/v1/*) |
| Authoring surface | HTML + CSS + JS | Markdown, builder DSL, JSON |
| Runtime | Your server (Node + Chromium) | Cloudflare Workers (we host) |
| Cold start | 500–2000ms (browser launch) | <100ms (Worker) |
| Container size | +200MB for Chromium | 0 — you call an HTTP endpoint |
| Memory per render | 200–500MB | bounded, single Worker request |
| Tagged PDF (PDF/UA-1) | Partial, lossy via "Tagged PDF" pref | Yes, by default |
| PDF/A archival | No (third-party post-processor) | Yes (PDF/A-2A by default) |
| veraPDF-validated output | No | Yes |
| Determinism | Subject to Chromium version drift | Byte-stable across versions |
| Agent-first | No | Yes — public skill file, MCP server, x402-payable |
| License | Apache-2.0 (browser proprietary) | Proprietary hosted; skill file MIT |
Same input, two outputs
A "Hello, Ada" report.
Puppeteer — spin up a browser, load HTML, print:
import puppeteer from "puppeteer";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(`
<!doctype html>
<html><body style="font-family: sans-serif; padding: 40px;">
<h1>Hello, Ada</h1>
<p>Welcome to the report.</p>
</body></html>
`);
await page.pdf({ path: "hello.pdf", format: "A4" });
await browser.close();
makesPDF — one HTTP call, no browser:
curl -X POST https://makespdf.com/api/v1/md \
-H "Authorization: Bearer $MAKESPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"markdown": "# Hello, Ada\n\nWelcome to the report."}' \
-o hello.pdf
The Puppeteer output is a PDF whose structure tree (if you remembered to enable it) is whatever Chromium decided to emit from your DOM — usually a flat run of Span elements with no real heading semantics, plus all your decorative <div>s tagged as if they were content. The makesPDF output is a tagged PDF with proper H1 / P structure elements you can verify by opening Acrobat's Tags pane.
Why people switch
- Chromium is a liability in your runtime. A PDF endpoint shouldn't ship 200MB of browser, a font cache, a sandbox config, and a memory ceiling that pages OOM under bursty load. Hosted means none of that lives in your image.
- HTML→PDF is the wrong abstraction for compliance. Chromium's "Tagged PDF" output is a best-effort transform of your DOM. It tags decorative divs, misses heading levels, and still requires manual remediation to pass a PDF/UA-1 audit. We render straight from semantic structure (
h1,p,table,figure) into a tagged tree, then validate with veraPDF. - Determinism. Same input → same bytes. Chromium silently changes layout between versions; your "stable" PDF endpoint isn't. makesPDF's renderer is a single owned pipeline — output is byte-stable across deploys.
- Cold start. Launching a browser per request is 500ms+ even pooled. A Worker round-trip is <100ms.
- Agents need an API, not a browser. Asking an LLM to "generate HTML + CSS that prints to a perfect A4" is the long way around. Asking it to emit markdown or a 30-token DSL string is the short way.
When Puppeteer is still the right call
- You're rendering an existing web page exactly as users see it (snapshot of a dashboard, a printable receipt that's already a polished web view). HTML→PDF is literally the job.
- You need arbitrary CSS features we don't support — complex CSS grid layouts, web fonts loaded from CSS
@font-face, JS-driven canvases. - You're in a regulated environment that forbids egress to a hosted service. Self-hosted Chromium stays in your VPC.
For most server-side or agent-driven document workloads, the hosted endpoint wins on container size, compliance, cold start, and operational surface area.
Migration
Most "Puppeteer that prints HTML to PDF" code paths are rendering a templated HTML string. Those map cleanly to markdown + the /api/v1/md endpoint, or to the builder DSL when you need precise layout (tables, multi-column, headers/footers). See the skill file for the DSL function reference and a recipe per document type.