Blog/Compare

Puppeteer vs a hosted HTML to PDF API

Compare··7 min read

Keep Puppeteer when you need a real browser session. Use a hosted HTML to PDF API when you already have HTML (or Markdown, or a template) and the document is the product. RelayPDF is one of those APIs: POST UTF-8 JSON with html, a public url, markdown, or templateId to https://api.relaypdf.com/v1/pdf and take a PDF. Product page: https://relaypdf.com/html-to-pdf. Operating-model compare: https://relaypdf.com/compare/puppeteer.

When to use which

Stay on Puppeteer (or Playwright) if any of these are true:

  • You must drive a login, MFA, or cookie jar you cannot flatten into HTML you already own.
  • You need Chrome flags, extensions, a specific binary, or CDP features an API will not expose.
  • You print intranet or loopback URLs. RelayPDF's URL source rejects private, loopback, and metadata hosts. Inline the HTML or keep the browser inside the network.
  • You want unlimited in-process iteration with no per-document debit. Price that against the people who keep Chrome running.

Move the printer out of process if:

  • The job is setContent(html) plus page.pdf(). You do not need a browser session.
  • Several services need the same print path and you do not want each one to vendor Chrome.
  • You want binary, a 24h download URL, or async plus webhook without writing that layer.
  • You also need merge, Office convert, barcodes, or zip as documented sibling endpoints.
  • Failed, invalid, unauthorized, and rate-limited requests should not be billed. RelayPDF's pricing page states they are never billed.

Wrapping Puppeteer in an internal API you own is still a valid PDF API. You become the vendor. If callers include agents as well as apps, RelayPDF's compare page puts CLI and MCP in the built-in column versus custom tooling.

page.pdf() defaults that bite

In current Puppeteer docs (v25), Page.pdf() prints the page with the print CSS media type, takes optional options, returns a Uint8Array, and writes to disk only if you pass path. Official PDFOptions: https://pptr.dev/api/puppeteer.pdfoptions.

  • printBackground defaults to false. Brand colors and table zebra stripes vanish unless you opt in or force -webkit-print-color-adjust.
  • timeout defaults to 30,000 ms (overridable via Page.setDefaultTimeout; 0 disables it).
  • format defaults to letter. preferCSSPageSize defaults to false, so @page size loses to the paper option.
  • displayHeaderFooter defaults to false. Header and footer HTML is separate from the page body.
  • To print with screen media, call page.emulateMediaType('screen') first.

The call is a few lines. The rest of the job is launch, context, page, setContent or goto, wait, pdf, close, retry, and the machine that held all of it.

Puppeteer: page.pdf()

import puppeteer from "puppeteer";
const browser = await puppeteer.launch({
  args: ["--no-sandbox", "--disable-dev-shm-usage"],
});
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
const pdf = await page.pdf({
  format: "letter",
  printBackground: true,
  margin: { top: "16mm", bottom: "16mm", left: "12mm", right: "12mm" },
});
await browser.close();

return pdf;

That is the happy path. Production adds a pool or a queue, crash handling, fonts on the image, tmpfs because Docker /dev/shm is often 64 MB, and a timeout story for pages that never go network-idle. On Lambda you add a layer or container image, a 512-2048 MB memory slider, and a cold start longer than the PDF. Those are general in-process Chromium costs, not RelayPDF benchmarks.

The same job as POST /v1/pdf

Documented at https://relaypdf.com/docs/pdf. Send exactly one source field (html, url, markdown, or templateId) as UTF-8 JSON. HTML is not Base64. Optional filename must end in .pdf. Option field names match across JSON, Node, and Python (printBackground, not print_background).

Default response is binary application/pdf. response: "url" returns JSON with a public download that expires in 24 hours. response: "async" plus an optional HTTPS callbackUrl defers the job. printBackground defaults to true on this endpoint, the opposite of Puppeteer's default.

RelayPDF: POST /v1/pdf and the Node SDK

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{"html":"<h1>Invoice #1042</h1><p>Total: $1,200.00</p>","filename":"invoice.pdf","options":{"format":"letter","printBackground":true,"margin":{"top":"16mm","bottom":"16mm","left":"12mm","right":"12mm"},"waitUntil":"networkidle0"}}'
  --output invoice.pdf

// Node from https://relaypdf.com/docs/quickstart/node

import { RelayPDF } from "@relaypdf/sdk";
const client = new RelayPDF({ apiKey: process.env.RELAYPDF_API_KEY });
const pdf = await client.pdf.fromHtml("<h1>Hello</h1>", { filename: "hello.pdf" });
await pdf.save("hello.pdf");

Rendering knobs (format, landscape, margins, headerTemplate / footerTemplate, pageRanges, width / height, waitUntil, timeout max 60,000 ms, waitForSelector, cookies, extraHTTPHeaders) are listed on https://relaypdf.com/docs/options. waitUntil values on the product page are load, domcontentloaded, networkidle0, and networkidle2. The options default for waitUntil is networkidle0. Jobs that fail to render return render_failed and are not billed.

Side-by-side

Operating-model comparison, not a scorecard. RelayPDF's compare page states the same split: Puppeteer gives maximum control, but you own browser lifecycle, retries, storage, auth, and billing.

TopicPuppeteer page.pdf()RelayPDF POST /v1/pdf
What you callpage.pdf(options) after launch / newPage / setContent or gotoOne HTTP request; Node helper client.pdf.fromHtml()
Process modelChromium in your VM, container, or LambdaManaged browser ops (you do not run Chrome)
printBackground defaultfalse (official PDFOptions)true (RelayPDF HTML-to-PDF docs)
Timeout30s default; 0 disablesoptions.timeout is Chromium budget, max 60s
SourcesAny page you can load, including sessions you driveExactly one of html, url, markdown, templateId. URL hosts must be public
ResponseUint8Array; optional path on diskbinary PDF (default), url (24h expiry), or async + optional callbackUrl
Headers / footersdisplayHeaderFooter + HTML templates (date, title, url, pageNumber, totalPages)headerTemplate / footerTemplate; placeholders pageNumber, totalPages, date, title
Wait strategyYou choose waitUntil on setContent/goto, plus your own selectorswaitUntil, waitForSelector, waitForTimeout (extra wait max 30s)
Auth to the printerNone. It is your processBearer API key; wallet debit per successful job
Sibling workYou add merge, zip, barcodes, Office convert yourselfDocumented separately: /v1/pdf/merge, /v1/convert, /v1/barcodes, /v1/zip, PDF tools
When it is the right toolYou need a browser, not a document endpointYou need a document, not a browser fleet

Memory, queues, Lambda

Open-source Chromium has no invoice. The bill is RAM, engineering time, and failed renders. A headless Chrome that can print modern CSS is commonly a few hundred megabytes resident, more under webfonts and canvas. In a long-lived Node service you either leak browsers or you write a pool (max, recycle policy, hang rule).

A queue is the next patch if checkout should not block on print. You then own visibility timeouts, poison messages, and retries for a 15-page report that died on page 14. Puppeteer is a browser driver. It will not do that.

Lambda and most FaaS are a poor home for Chromium unless you have already paid the tax: short runtime, small /tmp, sandbox versus Chrome's sandbox, cold starts. Teams pin a Chrome build, raise memory until SIGKILL stops, and still see flaky networkidle. None of this means Puppeteer is wrong on a box you control with a queue you already run. It means the marginal cost of the next thousand documents is rarely zero.

Migration mapping

Keep Puppeteer for session-driven flows. Jobs that only call setContent(html) plus page.pdf() are API candidates.

Watch the defaults when you map options. printBackground is false in official PDFOptions and true on RelayPDF /v1/pdf, so a lift-and-shift that omits the field will start printing backgrounds. timeout is 30s locally (0 disables) versus a 60s Chromium cap on the API. waitUntil default on RelayPDF options is networkidle0; extra waitForTimeout is capped at 30s. Header/footer placeholders overlap (pageNumber, totalPages, date, title) but the driver also documents url.

  1. Map options: format, margins, printBackground, headers, waitUntil, timeout. Defaults differ on printBackground and timeout.
  2. Send html as a JSON string. file:// or localhost URLs must become inline HTML or a public URL the API can fetch.
  3. Pick response mode: binary (the page.pdf() equivalent), url if callers should not hold a multi-MB body, or async plus callbackUrl instead of your queue.
  4. Office files go to POST /v1/convert. Merges go to POST /v1/pdf/merge. Do not cram those into /v1/pdf.

Limits

  • Chromium render budget maxes at 60 seconds. Giant, script-heavy pages that only finish after a long client wait do not belong here without cutting work.
  • URL fetch is public-web only. That is a security boundary.
  • You get the documented options, not every CDP method. Fix the DOM in your app, then POST the HTML.
  • This article does not claim OCR, e-sign, PDF/A, or HIPAA. Those are not on the source pages.

Pricing (verified)

Live rate card at https://relaypdf.com/pricing: prepaid wallet, no monthly tiers.

  • HTML / URL / Markdown to PDF or screenshot (/v1/pdf, /v1/images): $0.015 per successful operation.
  • Handlebars template to PDF (/v1/pdf + templateId): $0.015.
  • LibreOffice convert $0.04, wkhtmltopdf $0.025, PDF tools / barcodes / zip $0.005, AI template generation $0.05.
  • $5 credit at signup, no card required. Credit never expires. Top-up bonuses from 5% ($20 to $21) to 15% ($500 to $575).

A thousand HTML invoices at $0.015 is $15 plus any top-up bonus. Compare that to a 2 GB Lambda that sits warm, or an engineer afternoon on a Chrome bump. Run the numbers on your volume.

Request body and response modes: https://relaypdf.com/docs/pdf. Print controls: https://relaypdf.com/docs/options. Node one-pager: https://relaypdf.com/docs/quickstart/node.

Ready to generate?

One API for HTML, Markdown, URLs, and Office. REST, SDK, CLI, or MCP.