ScreenshotNeo

BlogHow-to

How to Deploy Puppeteer on Vercel with Node.js

Deploy Puppeteer in a Node.js Vercel Function with a separately supplied Chromium binary. Follow setup, deployment, troubleshooting, and a browser-free screenshot option.

By the ScreenshotNeo team30 September 20269 min read

How to Deploy Puppeteer on Vercel with Node.js

To deploy Puppeteer on Vercel, run it in a server-side Node.js Function and provide Chromium separately. Vercel’s guide recommends puppeteer-core with @sparticuz/chromium-min because the standard puppeteer package includes a browser and can exceed the function bundle limit. The browser binary still has to be available at runtime, and its version must match your Puppeteer setup. Vercel’s Puppeteer guide describes one archive-and-extract approach; it is an example architecture, not a rule for every project.

This walkthrough builds a Next.js App Router endpoint that accepts a URL and returns a PNG screenshot. Keep it server-side: browser code cannot launch a local Chromium process. For Node.js or TypeScript Functions, Node.js is Vercel’s default runtime when no other runtime is configured. See the runtime documentation and ScreenshotNeo API documentation.

1. Create a Node.js endpoint

Start with a Next.js project configured for deployment on Vercel. From the project root, install the browser automation packages:

The endpoint runs Chromium on the server and returns the captured image to the caller.
The endpoint runs Chromium on the server and returns the captured image to the caller.
npm install puppeteer-core @sparticuz/chromium-min

Use the package manager and lockfile already used by your project. Check the packages’ current compatibility notes before upgrading either dependency: Puppeteer and Chromium versions must work together, and browser packages can change their runtime and asset requirements.

In this example, the Chromium archive is supplied separately and made reachable through the CHROMIUM_REMOTE_URL environment variable. Set it to the archive location used by your deployment process. Vercel’s template demonstrates preparing an archive during installation or build, making it reachable to the function, extracting it when needed, and caching the executable path in a warm instance. The exact archive URL and packaging steps depend on the template and versions you choose; do not copy a stale asset URL without checking it.

Create app/api/screenshot/route.ts:

import chromium from "@sparticuz/chromium-min";
import puppeteer from "puppeteer-core";

export const runtime = "nodejs";
export const maxDuration = 60;

let executablePathPromise: Promise<string> | undefined;

async function getExecutablePath() {
  const archiveUrl = process.env.CHROMIUM_REMOTE_URL;
  if (!archiveUrl) {
    throw new Error("CHROMIUM_REMOTE_URL is not configured");
  }
  executablePathPromise ??= chromium.executablePath(archiveUrl);
  return executablePathPromise;
}

export async function GET(request: Request) {
  const input = new URL(request.url).searchParams.get("url");
  if (!input) {
    return Response.json({ error: "Pass a url query parameter" }, { status: 400 });
  }

  let target: URL;
  try {
    target = new URL(input);
  } catch {
    return Response.json({ error: "The url must be an absolute URL" }, { status: 400 });
  }
  if (!["http:", "https:"].includes(target.protocol)) {
    return Response.json({ error: "Only HTTP and HTTPS URLs are supported" }, { status: 400 });
  }

  let browser;
  try {
    const executablePath = await getExecutablePath();
    browser = await puppeteer.launch({
      args: chromium.args,
      executablePath,
      headless: true,
    });
    const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
    await page.goto(target.toString(), { waitUntil: "networkidle2", timeout: 25000 });
    const png = await page.screenshot({ type: "png", fullPage: true });

    return new Response(Buffer.from(png), {
      headers: {
        "Content-Type": "image/png",
        "Cache-Control": "no-store",
      },
    });
  } catch (error) {
    console.error("Screenshot capture failed", error);
    return Response.json({ error: "Screenshot capture failed" }, { status: 500 });
  } finally {
    await browser?.close().catch((error) => console.error("Browser close failed", error));
  }
}

The 60-second maxDuration is an example setting, not a recommended universal value. Choose a limit that fits your normal capture time and the current limit for your project and plan. If your framework does not support per-route configuration, use the appropriate vercel.json configuration documented by Vercel.

2. Prepare Chromium and configure the project

  1. Choose the binary source. Follow a maintained template or package instructions compatible with @sparticuz/chromium-min. For the archive pattern in Vercel’s template, prepare and host the Chromium archive, then configure its reachable URL as CHROMIUM_REMOTE_URL.
  2. Keep the URL server-side. Add CHROMIUM_REMOTE_URL as a project environment variable for the deployment environments that need it. Do not put private storage credentials in client code or commit secrets to the repository.
  3. Match the runtime and packages. Keep this route on Node.js, and check the package and binary compatibility requirements. Avoid assuming that a Chromium executable from your workstation will run in Vercel’s function environment.
  4. Review output size. The function bundle includes code, libraries, and files traced into it. Vercel’s current limits documentation describes bundle constraints; inspect the live limits and build output rather than relying on a remembered number. The guide currently describes a 250 MB function bundle constraint. Review Vercel Function limits.
  5. Run locally, then deploy. Test the route with a URL you control. Deploy from the project root using the Vercel CLI. The CLI documentation shows vercel --prod for production deployment.
vercel --prod

After deployment, open the Function’s logs and deployment details if it fails. Confirm that the intended branch and production deployment were used, that the environment variable exists in that environment, and that the function includes the expected dependencies.

3. Call the endpoint

Pass an absolute HTTP or HTTPS URL as the url query parameter. URL-encode it when constructing requests programmatically; command-line tools can handle encoding as shown here.

cURL

curl --get 'https://YOUR_PROJECT.vercel.app/api/screenshot' \
  --data-urlencode 'url=https://example.com' \
  --output screenshot.png

Python

import requests

response = requests.get(
    "https://YOUR_PROJECT.vercel.app/api/screenshot",
    params={"url": "https://example.com"},
    timeout=75,
)
response.raise_for_status()
with open("screenshot.png", "wb") as image:
    image.write(response.content)

Node.js

const endpoint = new URL("https://YOUR_PROJECT.vercel.app/api/screenshot");
endpoint.searchParams.set("url", "https://example.com");

const response = await fetch(endpoint);
if (!response.ok) {
  throw new Error(`Capture failed: ${response.status} ${await response.text()}`);
}
await import("node:fs/promises").then(({ writeFile }) =>
  writeFile("screenshot.png", Buffer.from(await response.arrayBuffer()))
);

Protect a production endpoint from arbitrary public use. Add authentication and rate limiting appropriate to your application, and restrict which URLs it may capture. A public endpoint that accepts arbitrary URLs can be abused to make your function fetch internal or sensitive destinations. Validate hostnames against an allowlist when the use case permits it, and do not treat checking only the URL scheme as complete protection.

4. Tune capture behavior

The example waits for networkidle2, which can suit pages that settle after loading but can delay or fail on sites with ongoing requests. Pick the lightest wait condition that produces the result you need. Puppeteer navigation supports lifecycle choices such as domcontentloaded and load; a page-specific selector or short explicit delay can be a better readiness signal for highly dynamic content.

Need Adjustment Trade-off
Faster capture Use a less strict navigation condition or wait for a known selector. The page may not have finished rendering images or client-side content.
Long pages Use fullPage: true or capture a selected element. Large pages consume more memory and produce larger image responses.
Consistent dimensions Set viewport width and height before navigation. Responsive content may differ at other viewport sizes.
Downloads or PDFs Return an appropriate content type and set a bounded output strategy. PDFs and high-resolution pages can take longer and use more memory.
Repeat requests Consider caching the final output when the target and capture options are stable. Cached images can become stale; define a refresh policy.

For reliability, use explicit navigation and screenshot timeouts, close the browser in a finally block, and log enough context to diagnose failures without logging secrets. Reuse the extracted executable path at module scope when the function instance remains warm, as in the example. Each invocation should still manage its page and browser lifecycle carefully; warm instances are an optimization, not a guarantee.

Or skip the browser setup

If your goal is to get a screenshot rather than operate Chromium, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF, and its parameter names also work with those used by other screenshot APIs. The following example saves a WebP response:

A screenshot API can handle common overlays before returning the captured page.
A screenshot API can handle common overlays before returning the captured page.
curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. See the API documentation and get 1,000 free screenshots a month with no card.

Troubleshooting

Symptom Likely cause What to check
Chromium fails to launch The executable is missing, the archive URL is wrong, or the binary does not match the runtime. Check CHROMIUM_REMOTE_URL, archive availability, extraction logs, and package compatibility. Compare with the selected template’s archive workflow.
“Could not find Chrome” puppeteer-core does not download a browser automatically. Ensure the function obtains a Chromium executable and passes its path to puppeteer.launch.
Build or deployment exceeds size limits The bundled dependencies or traced files are too large. Inspect the function output and included files. Use the lightweight package pattern from the Vercel guide and keep the browser asset provisioned as intended by your chosen approach.
Function returns 504 or times out Browser startup, navigation, or rendering exceeds the configured function duration. Reduce navigation waits, constrain capture size, set a realistic duration within the current project limit, and inspect the function logs. Vercel’s defaults and maximums vary by configuration and plan. Check current duration settings.
Screenshot is blank or incomplete The site renders after the selected wait condition, blocks automation, or requires user interaction. Wait for a page-specific selector, inspect navigation errors, and test a permitted page. Some sites show bot checks or require authentication.
Works locally but fails after deploy Local and deployed browser binaries or environment variables differ. Verify the production environment variable, deployed dependencies, archive reachability, and function logs. Reproduce with the deployment’s package and binary combination.
New deployment seems unchanged A different branch or deployment was published, or the request reaches an older URL. Inspect the deployment details, production domain assignment, and logs; confirm the intended commit and run the production deploy command from the correct project.

Performance, reliability, and cost

Browser startup and asset extraction add work before page capture. Keeping the extracted executable path in module memory can avoid repeating that step on a warm instance, but Vercel may start a fresh instance at any time. Do not rely on in-memory state for correctness. Measure your own target pages and deployment setup; the cited sources do not establish a universal Puppeteer performance benchmark.

Large viewport dimensions, full-page images, heavy pages, and PDFs can increase processing time and memory use. Bound request and navigation time, validate the target, and return a clear error when capture fails. For high volume or long-running jobs, check current function duration, memory, concurrency, and pricing documentation before choosing synchronous request handling. Function limits and billing rules can change, so avoid hard-coding a plan assumption into the application.

ScreenshotNeo offers a simpler cost model for the screenshot use case: only clean shots are billed; bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing state. Listed plans are Free with 1,000 shots per month, Starter at $5 for 3,000, Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Check the product site for current plan details.

FAQ

Can I launch Puppeteer in a Vercel Edge Function?

This guide uses a Node.js Function. Use that runtime for this Chromium deployment pattern; browser process support and runtime capabilities are not interchangeable.

Do I need Next.js?

No. The example uses Next.js routing, while Vercel also supports JavaScript and TypeScript Functions on Node.js. Adapt the handler shape to your framework and confirm its function configuration conventions.

Can I return a PDF instead of a PNG?

Yes. Puppeteer can generate PDFs, but set the response content type to application/pdf, choose PDF options deliberately, and allow for the added time and memory that rendering may need.

Does a successful deployment prove every target site will capture?

No. Sites differ in load behavior, authentication, bot checks, and client-side rendering. Test representative pages and provide useful errors for pages your endpoint cannot capture.