← All guides

Puppeteer PDF alternative: when running Chromium yourself stops paying off

Puppeteer is the obvious way to make PDFs from HTML in Node, and for many teams it is the right one. But "spin up headless Chrome and call page.pdf()" hides a surprising amount of operational work. This guide is an honest look at what self-hosting costs, how to do it well if you stay, and the signals that it is time to hand it off.

What a production Puppeteer setup actually needs

The tutorial version launches a browser, renders, and closes it. A production version needs most of the following.

A shared browser and a concurrency limit

Launching Chromium per request adds latency and memory spikes. Instead, keep one browser alive and cap how many pages render at once:

import puppeteer from "puppeteer";

let browserPromise = null;
let active = 0;
const MAX_PAGES = 4;
const waiting = [];

function getBrowser() {
  if (!browserPromise) {
    browserPromise = puppeteer.launch({ args: ["--disable-dev-shm-usage"] });
    browserPromise.then((b) => b.on("disconnected", () => { browserPromise = null; }));
  }
  return browserPromise;
}

async function acquire() {
  if (active < MAX_PAGES) { active++; return; }
  await new Promise((resolve) => waiting.push(resolve));
  active++;
}

function release() {
  active--;
  const next = waiting.shift();
  if (next) next();
}

export async function renderPdf(html) {
  await acquire();
  const browser = await getBrowser();
  const page = await browser.newPage();
  try {
    await page.setRequestInterception(true);
    page.on("request", (req) => {
      const url = req.url();
      if (url.startsWith("https://") || url.startsWith("data:")) req.continue();
      else req.abort();
    });
    await page.setContent(html, { waitUntil: "networkidle0", timeout: 15000 });
    return await page.pdf({ format: "A4", printBackground: true, timeout: 30000 });
  } finally {
    await page.close().catch(() => {});
    release();
  }
}

That is already more code than most teams expect, and it still leaves gaps.

Things the snippet does not handle

  • Memory growth. Long-lived Chromium processes tend to grow. Many teams recycle the browser after N renders or on a timer.
  • Crashes and hangs. A page that never settles can block a slot forever unless every step has a timeout and the browser is killed when one fires.
  • SSRF. Blocking non-HTTPS requests is a start, but public hostnames can still resolve to internal IPs. If users control HTML or URLs, you need DNS-aware egress filtering or a network-isolated renderer.
  • Fonts. Container images need font packages for CJK, Arabic, Hebrew, Thai and emoji, or users see empty boxes.
  • Image size and upgrades. Chromium and its libraries add hundreds of megabytes to images. Each Puppeteer major version pins a new Chrome; upgrading occasionally changes layout or breaks flags.
  • Scaling. Rendering is CPU-bound and bursty. Month-end invoice runs or report exports can saturate a box that idles the rest of the month.
  • Serverless. On Lambda or Vercel you need a trimmed Chromium build, bigger memory settings, and you pay for cold starts that unpack the browser.

When self-hosting is the right call

Keep Puppeteer if:

  • Your HTML must never leave your network for compliance reasons.
  • You render very high volumes continuously and already operate similar services, so the marginal cost is low.
  • You need full browser automation beyond rendering, such as logging into a site through several steps before printing.
  • You already run it, it is stable, and nobody is getting paged.

Signs it is time to stop

  • You have written (or are about to write) a browser pool, a queue, and a watchdog.
  • PDF rendering is the only reason your images include Chromium, or the reason a serverless app needs a container.
  • Incidents mention "Chrome", "zombie processes" or "out of memory".
  • You are adding fonts one support ticket at a time.
  • Engineers are spending time on renderer upgrades instead of the product.

The PDFOven way

PDFOven is the same Chromium print pipeline you get from Puppeteer, exposed as an API. The renderPdf function above becomes:

export async function renderPdf(html) {
  const res = await fetch("https://pdfoven.com/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.PDFOVEN_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      html,
      options: { format: "A4", print_background: true },
      timeout_ms: 30000,
    }),
  });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(error.code + ": " + error.message);
  }
  return Buffer.from(await res.arrayBuffer());
}

Mapping Puppeteer options

Puppeteer PDFOven
page.setContent(html) html
page.goto(url) url
page.waitForSelector(sel) wait_for_selector
format, landscape, scale options.format, options.landscape, options.scale
margin: { top, ... } options.margin (for example "0.5in")
printBackground options.print_background
headerTemplate / footerTemplate options.header_template / options.footer_template
page.screenshot() POST /v1/image with width, height, full_page, device_scale_factor
page.addStyleTag({ content }) css

Header and footer templates support the same <span class="pageNumber"></span> and <span class="totalPages"></span> classes you already use.

Beyond raw HTML, you can send markdown, an inline Handlebars template with data, or a saved template_id with data, so the templating layer can move out of your app too.

Cost comparison, honestly

We will not invent a benchmark. Compare your own numbers: the instance or function cost of the memory Chromium needs, plus engineering time on the issues above, against a plan price. PDFOven is free for 100 renders per month and $12, $39 or $99 for 2,000, 15,000 or 60,000 renders. If you are above that volume with a stable setup, self-hosting may well be cheaper. If you are below it, the API is usually less work.

Errors are JSON ({ "error": { "code", "message" } }), quota exhaustion is 429 quota_exceeded, and rendered files are never stored. Try a document in the free HTML to PDF tool before changing any code.

Skip the browser plumbing

PDFOven runs Chromium for you. Send HTML, Markdown, a URL or JSON to a template and get a PDF back. 100 free renders every month.

Get a free API key