# HTML to PDF on AWS Lambda

Convert HTML to PDF on AWS Lambda with @sparticuz/chromium, or skip the size and cold-start costs with one fetch call to a PDF API and an S3 upload.

Lambda is a common home for PDF jobs. An SQS message arrives, a statement gets rendered, and the file lands in S3.

There are two ways to do the rendering. You can run Chromium inside the function, or you can call a rendering API from it.

**Quick answer:** from any Node.js 18+ Lambda, call the API with the built-in `fetch`. No layers or dependencies needed.

```bash
curl -X POST https://html-pdf.com/v1/pdf \
  -H "Authorization: Bearer $HTML_PDF_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Hello</h1>"}' -o hello.pdf
```

## The DIY route: @sparticuz/chromium + puppeteer-core

This section shows how to run Chromium inside Lambda yourself, and what it costs you.

Regular Puppeteer downloads a full Chrome that's too big and isn't built for Lambda's environment. The usual fix is `@sparticuz/chromium`, a Chromium build packaged for Lambda, combined with `puppeteer-core`.

```js
// index.mjs
import chromium from "@sparticuz/chromium";
import puppeteer from "puppeteer-core";

export const handler = async (event) => {
  const browser = await puppeteer.launch({
    args: chromium.args,
    executablePath: await chromium.executablePath(),
    headless: true,
  });
  try {
    const page = await browser.newPage();
    await page.setContent(event.html, { waitUntil: "networkidle0" });
    const pdf = await page.pdf({ format: "A4", printBackground: true });
    return { statusCode: 200, body: Buffer.from(pdf).toString("base64"), isBase64Encoded: true,
             headers: { "Content-Type": "application/pdf" } };
  } finally {
    await browser.close();
  }
};
```

Keep the `puppeteer-core` version matched to the Chromium version the package ships, or launches fail. It works, but there are tradeoffs.

### Package size

Lambda's limits (per AWS docs) are 50 MB zipped for direct upload, 250 MB unzipped for the function plus all its layers, and 10 GB for a container image. Chromium alone takes a large share of the 250 MB budget, so most teams put it in a layer or switch to a container image.

You'll also need fonts for non-Latin scripts and emoji, which adds more weight.

### Memory and CPU

Lambda allows 128 MB to 10,240 MB of memory, with CPU scaled to memory (about one vCPU at 1,769 MB). Chromium is memory-hungry. Under about 1.5-2 GB, renders get slow or crash on heavy pages, and you pay for that memory on every invocation.

### Cold starts

Each cold start decompresses Chromium and launches the browser before any rendering happens. That adds seconds to the first request. Provisioned concurrency hides it, at extra cost.

### Maintenance

Chromium security updates, Puppeteer upgrades, version pinning, fonts and Amazon Linux changes are all yours to track.

If PDFs are core to your product and volume is high, DIY can be worth it. If you just need correct PDFs, an API removes all of the above.

## The API way: fetch from a Node.js Lambda

This section gives you two ready-to-deploy handlers that call html-pdf.com instead of running a browser.

Node.js 18+ runtimes (including Node.js 20) have `fetch` built in, so there are no dependencies. html-pdf.com renders with headless Chromium and bills per page (1 credit = 1 PDF page).

Get a key at https://html-pdf.com/login (100 free credits) and store it as an environment variable `HTML_PDF_KEY`. Ideally, source it from Secrets Manager or SSM Parameter Store.

### Return the PDF through API Gateway

```js
// index.mjs (Node.js 20.x)
export const handler = async (event) => {
  const { html } = JSON.parse(event.body ?? "{}");

  const res = await fetch("https://html-pdf.com/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.HTML_PDF_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, format: "A4", margin: "15mm", print_background: true }),
  });

  if (!res.ok) {
    // { "error": { "code", "message", "hint" } }
    return { statusCode: res.status, headers: { "Content-Type": "application/json" },
             body: await res.text() };
  }

  const pdf = Buffer.from(await res.arrayBuffer());
  return {
    statusCode: 200,
    isBase64Encoded: true,
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": 'attachment; filename="document.pdf"',
    },
    body: pdf.toString("base64"),
  };
};
```

With an HTTP API or a Lambda function URL this works as is. With a REST API, add `application/pdf` (or `*/*`) to the API's binary media types so API Gateway decodes the base64 body.

Synchronous Lambda responses are capped at 6 MB, and base64 adds about a third. Large PDFs are better stored in S3.

### Upload to S3 with AWS SDK v3

The Node.js 20 runtime includes AWS SDK v3, so `@aws-sdk/client-s3` is available without bundling.

```js
// index.mjs (Node.js 20.x)
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({});
const Bucket = process.env.BUCKET;

export const handler = async (event) => {
  const { id, html } = event; // e.g. from SQS, Step Functions or a direct invoke

  const res = await fetch("https://html-pdf.com/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.HTML_PDF_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, format: "Letter", prefer_css_page_size: true }),
  });
  if (!res.ok) throw new Error(`PDF failed: ${res.status} ${await res.text()}`);

  const Key = `pdfs/${id}.pdf`;
  await s3.send(new PutObjectCommand({
    Bucket, Key,
    Body: Buffer.from(await res.arrayBuffer()),
    ContentType: "application/pdf",
  }));

  const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key }), { expiresIn: 3600 });
  return { key: Key, url, pages: res.headers.get("X-Pages") };
};
```

Give the function's role `s3:PutObject` and `s3:GetObject` on the bucket. Check whether `@aws-sdk/s3-request-presigner` is included in your runtime version, and bundle it if not. Since the function mostly waits on I/O, 256-512 MB of memory is plenty.

Don't need S3? Send `"response": "url"` and the API returns JSON `{ "url": "...", "expires_at": "..." }` with a link valid for 24 hours.

## DIY vs API at a glance

This table sums up the tradeoffs.

| | @sparticuz/chromium | HTML-to-PDF API |
|---|---|---|
| Deployment | Layer or container image with Chromium | No dependencies |
| Memory | Typically 1.5 GB+ | Minimal |
| Cold start | Browser launch each cold start | Plain HTTP call |
| Updates | You maintain Chromium/Puppeteer | Managed |
| Cost | Lambda GB-seconds | Lambda time + 1 credit per page |

## Options and limits

These are the options you'll use most, and the limits to plan around.

The API accepts `html` or a public `url`, plus `format` (A3 to A6, Letter, Legal, Tabloid), `landscape`, `margin`, `header_html`/`footer_html` with `pageNumber` and `totalPages`, `wait_for_expression` for JavaScript-rendered charts, and `max_pages`.

Requests can be up to 20 MB with 30 seconds per render. A `402 insufficient_credits` error means your balance is empty.

## FAQ

### Can I run Puppeteer on AWS Lambda?

Yes, with `@sparticuz/chromium` and a matching `puppeteer-core` version. Expect a large package, 1.5 GB or more of memory and slower cold starts.

### How do I generate a PDF in Lambda without Chromium?

Call `POST https://html-pdf.com/v1/pdf` with the built-in `fetch` and your HTML. The response is the PDF, which you can return through API Gateway or upload to S3.

### Is there a free tier?

You get 100 free credits once you confirm your email, with no card. One credit is one PDF page, and packs after that cost $0.01 to $0.005 per page.

### Does it support page numbers?

Yes. Use `header_html` or `footer_html` with the `pageNumber` and `totalPages` classes, and set a margin so they have room.

Full reference: https://html-pdf.com/docs. More Node.js patterns: https://html-pdf.com/html-to-pdf-nodejs. Invoice templates: https://html-pdf.com/generate-invoice-pdf.
