html4pdf vs running Chromium in a Lambda

Every serverless platform has a blog post about this, and every one of them is longer than it should be. That length is the comparison.

The short answer

It works, it is free, and it is the single most fiddly part of most codebases that do it. If your function is already near the size limit or your users wait on the first request, the trade has stopped paying.

What serverless Chromium is

The pattern is puppeteer-core, which is Puppeteer without a bundled browser, plus a Chromium build stripped down to fit a function package, usually @sparticuz/chromium. You launch it with a specific set of flags, render, and close. It is free, it runs inside your own infrastructure, and it is the standard answer on AWS Lambda, Vercel and Netlify.

Side by side

 serverless Chromiumhtml4pdf
Package sizeClose to the 50MB limit before your own codeNothing added
Cold startSeveral seconds to unpack and boot the browserNone
Memory1GB or more configured per functionNot your concern
MaintenanceThe Chromium build must track the runtime and the libraryNone
FontsYou load them yourself, the image has almost noneChrome with a normal font stack
TimeoutsA render that hangs burns the whole function timeoutBounded by the request
CostFunction time, which includes browser bootFrom $0.033 per document
DataNever leaves your accountRendered in memory, never stored
Edge runtimeNot possibleWorks, it is a fetch

Choose serverless Chromium when

  • Documents must stay inside your own cloud account for policy reasons.
  • You generate enough volume that function time is cheaper than per-document pricing.
  • You have already done the work and it is stable.
  • You need to control the exact Chrome version and flags.

Choose html4pdf when

  • Your function is bumping the size limit and you are deleting dependencies to fit.
  • A user is waiting on the request and the cold start is the slowest part of it.
  • You are debugging missing fonts and emoji in a stripped Chromium image.
  • You want this feature to stop being a thing anyone thinks about.

What the change looks like

The browser lifecycle, the flags and the stripped Chromium dependency all go. What is left is the request.

before.ts
import chromium from "@sparticuz/chromium"
import puppeteer from "puppeteer-core"

const browser = await puppeteer.launch({
  args: chromium.args,
  executablePath: await chromium.executablePath(),
  headless: true,
})

const page = await browser.newPage()
await page.setContent(html, { waitUntil: "networkidle0" })
const pdf = await page.pdf({ format: "A4", printBackground: true })
await browser.close()
after.ts
const res = await fetch("https://api.html4pdf.com/generate-pdf", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.HTML4PDF_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html,
    responseFormat: "binary",
    options: { format: "A4", printBackground: true },
  }),
})

const pdf = Buffer.from(await res.arrayBuffer())

See the code

The working example, in the language this decision is being made in.

serverless Chromium: common questions

Can I run Puppeteer on Vercel?

Yes, with puppeteer-core and @sparticuz/chromium. Expect to spend time on the function size limit, the memory setting and cold starts, and expect to revisit it when the runtime version changes.

Why is my serverless PDF missing fonts?

The stripped Chromium image ships almost no fonts, so anything not embedded falls back to whatever is there, which is often nothing. You load fonts yourself, or embed them in the document.

How slow is the cold start?

Several seconds before your code runs, because the browser has to be unpacked and started. Warm invocations are much faster, which is exactly the shape users notice.

Is it cheaper than an API?

At volume, yes. At low or spiky volume the function time spent booting a browser, plus the memory you had to provision for it, closes the gap faster than people expect.

Try it before you decide

New accounts get 10 free credits, no card. Send the HTML you already have and compare the output against whatever you are using now.

Get API key

Other comparisons