Screenshots & PDFs in serverless functions

Running Puppeteer or Playwright inside AWS Lambda, Vercel or Netlify functions is a classic source of pain: the full Chromium build is too big for the package, it needs lots of memory, and every cold start launches a browser. This guide covers the options and gives working handlers that call a hosted render API instead.

Why it's hard

Your options

OptionGood for
@sparticuz/chromium + puppeteer-coreStaying fully self-hosted: a compressed Chromium built for Lambda that fits the zip limit
You track Chromium/Puppeteer version pairs and still pay the memory and cold-start cost
Lambda container image (up to 10 GB)Any browser, any fonts: package regular Playwright or Puppeteer in Docker
Bigger images mean slower cold starts; you maintain the image
A hosted API (Shotmint, …)No browser in the function at all: one HTTP call, a tiny bundle, no extra memory
Pages are rendered by a third party; costs per render

If you need to render private pages that only exist inside your VPC, self-host. Otherwise a hosted API keeps the function small and fast, and the code below has zero dependencies.

AWS Lambda (Node.js 20+): URL to PDF

Works behind a Function URL or API Gateway. Set SHOTMINT_KEY as an environment variable (or read it from Secrets Manager).

// index.mjs
export const handler = async (event) => {
  const url = event.queryStringParameters?.url ?? "https://example.com";
  const r = await fetch("https://shotmint.creasework.com/v1/pdf?" + new URLSearchParams({ url, format: "A4" }), {
    headers: { "X-API-Key": process.env.SHOTMINT_KEY },
  });
  if (!r.ok) return { statusCode: r.status, body: await r.text() };
  const buf = Buffer.from(await r.arrayBuffer());
  return {
    statusCode: 200,
    isBase64Encoded: true,
    headers: { "Content-Type": "application/pdf" },
    body: buf.toString("base64"),
  };
};

AWS Lambda (Python): screenshot with the standard library only

No layer and no requests needed.

import base64, os, urllib.parse, urllib.request

def handler(event, context):
    url = (event.get("queryStringParameters") or {}).get("url", "https://example.com")
    q = urllib.parse.urlencode({"url": url, "width": 1280, "format": "png"})
    req = urllib.request.Request("https://shotmint.creasework.com/v1/screenshot?" + q,
                                 headers={"X-API-Key": os.environ["SHOTMINT_KEY"]})
    with urllib.request.urlopen(req, timeout=60) as r:
        body = r.read()
    return {"statusCode": 200, "isBase64Encoded": True,
            "headers": {"Content-Type": "image/png"},
            "body": base64.b64encode(body).decode()}

Vercel / Next.js (App Router): screenshot route

Streams the image straight through, so nothing is buffered in the function. cache_ttl makes repeat requests for the same URL free for a day, and the CDN header lets Vercel cache it too.

// app/api/screenshot/route.js
export const runtime = "nodejs";

const ALLOWED = ["example.com", "www.example.com"]; // don't let strangers spend your credits

export async function GET(req) {
  const url = new URL(req.url).searchParams.get("url") || "https://example.com";
  if (!ALLOWED.includes(new URL(url).hostname)) return new Response("host not allowed", { status: 403 });
  const r = await fetch("https://shotmint.creasework.com/v1/screenshot?" + new URLSearchParams({
    url, width: "1280", format: "webp", cache_ttl: "86400",
  }), { headers: { "X-API-Key": process.env.SHOTMINT_KEY } });
  if (!r.ok) return new Response(await r.text(), { status: r.status });
  return new Response(r.body, {
    headers: { "Content-Type": r.headers.get("content-type"), "Cache-Control": "public, s-maxage=86400" },
  });
}

Gotchas

Try the output first, no key needed: website screenshot, full-page screenshot and URL to PDF. Related: Puppeteer alternative, Node.js guide, Python guide.

What it costs

One screenshot or PDF = one credit. €5 buys 2,000 renders, credits never expire and there's no subscription. Cache hits with cache_ttl and failed renders are free.

Try it with 100 free renders

No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire).

Get a free API key Full API reference