Serverless functions are great for most backend work. PDF generation is not most backend work.

If you have tried running Puppeteer or Playwright inside a Vercel Edge Function or an AWS Lambda, you already know the pain. Binary size limits, memory caps, cold start timeouts. The rendering engine that works perfectly on your local machine becomes a liability the moment you deploy it.

There is a better way.

The Problem: Chromium Does Not Belong in Serverless

Most PDF generation libraries work by launching a headless browser, loading HTML, and printing to PDF. That browser is Chromium, and Chromium is big.

Here is what you are dealing with:

  • Binary size: Chromium weighs ~130MB compressed. AWS Lambda has a 250MB uncompressed deployment limit. After adding your application code, dependencies, and the Chromium binary, you are pushing right against that ceiling.
  • Memory: Rendering a moderately complex page needs 300-500MB of RAM. Lambda functions default to 128MB. Even at 1GB, a busy function can OOM on multi-page documents.
  • Cold starts: Loading Chromium from scratch takes 5-10 seconds. Your users are waiting for a PDF, not a progress bar.
  • Vercel Edge: Forget it entirely. Edge Functions run on V8 isolates with no filesystem access and a 1MB size limit. Chromium is not an option.

Some teams try @sparticuz/chromium, a stripped-down Chromium build for Lambda. It works for simple pages but still uses ~170MB of your deployment budget and needs 512MB+ of memory. You are fighting the platform instead of building your product.

Why This Keeps Biting Teams

The real trap is that it works in development. Your local machine has plenty of memory and disk. Puppeteer launches in under a second. The PDF looks perfect.

Then you deploy:

  • The Lambda layer is 48MB too large. You spend an afternoon shaving dependencies.
  • Cold starts add 8 seconds to the first PDF request. You add a warming cron job.
  • A customer generates a 30-page report and the function OOMs. You bump memory to 2GB and watch your AWS bill climb.
  • You add a second document type. Now you need different Chromium flags for each. The configuration matrix grows.

Every fix leads to the next problem. You are not building a PDF feature anymore. You are operating a rendering infrastructure inside a platform designed to run small, fast functions.

The Practical Solution: Offload Rendering to an API

The cleanest approach for serverless environments is to not render PDFs at all. Instead, send your data to a service built for rendering and get a PDF URL back.

Your serverless function becomes a thin proxy:

  1. Receive the request from your frontend
  2. Gather the data needed for the document
  3. POST the data to a PDF generation API
  4. Return the PDF URL to the frontend

No Chromium. No binary layers. No memory spikes. Your function stays small, fast, and within every platform limit.

Transactional.dev is built exactly for this. You design templates with HTML and Tailwind CSS, then generate PDFs through a single API call. The rendering happens on dedicated infrastructure optimized for it.

AWS Lambda Example

// handler.js
export const handler = async (event) => {
  const body = JSON.parse(event.body);

  const response = await fetch(
    "https://api.transactional.dev/v1/generate",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-token": process.env.TRANSACTIONAL_API_KEY,
      },
      body: JSON.stringify({
        documentId: "your-template-uuid",
        variables: {
          customer_name: body.customerName,
          invoice_number: body.invoiceNumber,
          line_items: body.items,
          total: body.total,
        },
      }),
    }
  );

  const { url } = await response.json();

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ pdfUrl: url }),
  };
};

This function is under 1KB. It deploys in seconds. It uses ~50MB of memory. It handles any document size because the heavy lifting happens elsewhere.

Vercel Edge Function Example

// app/api/generate-pdf/route.ts
import { NextResponse } from "next/server";

export const runtime = "edge";

export async function POST(request: Request) {
  const body = await request.json();

  const response = await fetch(
    "https://api.transactional.dev/v1/generate",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-token": process.env.TRANSACTIONAL_API_KEY!,
      },
      body: JSON.stringify({
        documentId: "your-template-uuid",
        variables: body,
      }),
    }
  );

  const { url } = await response.json();
  return NextResponse.json({ pdfUrl: url });
}

This runs on the Edge runtime. No Node.js APIs needed, no filesystem, no binary dependencies. It works because it is just an HTTP call.

Vercel Serverless Function (Node.js Runtime)

If you prefer the Node.js runtime on Vercel:

// pages/api/generate-pdf.ts
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).end();
  }

  const response = await fetch(
    "https://api.transactional.dev/v1/generate",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-token": process.env.TRANSACTIONAL_API_KEY!,
      },
      body: JSON.stringify({
        documentId: "your-template-uuid",
        variables: req.body,
      }),
    }
  );

  const { url } = await response.json();
  res.status(200).json({ pdfUrl: url });
}

Implementation Notes

Cold starts are irrelevant: Since your function is just making an HTTP call, cold starts drop to under 200ms. No binary to load, no browser to initialize.

Concurrency is not a problem: Each function invocation is a lightweight HTTP request. You can handle hundreds of concurrent PDF generations without worrying about memory.

Timeout headaches disappear: Vercel Edge Functions have a 25-second timeout. Lambda defaults to 3 seconds (configurable up to 15 minutes). A PDF API call typically completes in 1-3 seconds, well within any limit.

Environment variables are enough: Store your API key in your platform environment variables. No binary paths, no Chromium executable locations, no layer ARNs.

Common Mistakes

Trying to make Chromium fit: Teams spend weeks optimizing Chromium for Lambda: custom layers, stripped builds, memory tuning. That time is better spent on your actual product.

Using the wrong runtime: If you go with an API approach, you do not need the Node.js runtime. Edge Functions are faster and cheaper. Use them unless you have a specific reason not to.

Not handling API errors: Your PDF API call can fail (network issues, invalid template, bad variables). Always return a meaningful error to your frontend instead of a generic 500.

Hardcoding template IDs: Store your document IDs in environment variables or a config file. When you update a template, you do not want to redeploy your function.

Conclusion

Serverless environments are built for small, fast, stateless functions. PDF rendering with Chromium is none of those things. Trying to force them together creates an ongoing maintenance burden that scales with every new document type.

The practical solution is to separate concerns: let your serverless function handle business logic and let a dedicated service handle rendering.

Transactional.dev gives you a single API endpoint that turns your HTML/Tailwind templates into PDFs. Your Lambda or Vercel function stays lean, fast, and cheap.

Get started with Transactional.dev and stop fighting Chromium in serverless.