NODE.JS GUIDE

How to Capture Screenshots in Node.js Without Puppeteer

Puppeteer and Playwright are powerful but heavyweight. Here is how to capture production-quality screenshots from Node.js using a REST API — no browser to install, no memory leaks, works on serverless.

Get Free API Key

The Problem with Puppeteer in Production

Puppeteer and Playwright are the go-to tools for headless browser automation in Node.js. For local development and testing, they work great. But deploying them to production introduces a set of challenges that quickly consume engineering time.

First, the binary size. Chromium is roughly 300MB. On AWS Lambda, your deployment package limit is 250MB unzipped — Chromium alone blows past it. Workarounds exist (Lambda layers, EFS mounts, custom builds) but each adds operational complexity. On serverless platforms like Vercel or Netlify Functions, Chromium is simply not supported in standard function runtimes.

Second, memory. Each Chromium instance uses 150-400MB RAM under load. If you are running screenshots in response to user requests, you need to manage a browser pool — launching, reusing, and killing instances carefully to avoid leaks. This is essentially writing a browser management service as a side project within your main project.

Third, reliability. Chromium crashes. Pages hang. JavaScript errors in the target page can freeze the browser context. You need watchdog timers, page timeout handling, browser restart logic, and health checks. It is a lot of infrastructure for what should be a simple "give me a screenshot of this URL" operation.

The Screenshot API Approach

A screenshot API offloads all browser infrastructure to a managed service. Your Node.js code makes an HTTPS request and gets back a PNG or JPEG. No Chromium on your server, no browser pool management, no memory leak debugging at 2AM.

SnapAPI is designed for this. One API key, four endpoints (screenshot, scrape, extract, PDF), works from any Node.js environment including Lambda, Vercel, and Cloudflare Workers (fetch-compatible).

Basic Usage: Node.js fetch (no dependencies)

// Works in Node.js 18+ with native fetch
const SNAP_KEY = process.env.SNAPAPI_KEY;

async function screenshot(url, options = {}) {
  const params = new URLSearchParams({
    access_key: SNAP_KEY,
    url,
    width:     options.width     ?? 1280,
    height:    options.height    ?? 800,
    format:    options.format    ?? 'png',
    full_page: options.fullPage  ?? false,
    delay:     options.delay     ?? 0,
  });

  const res = await fetch(`https://api.snapapi.pics/screenshot?${params}`);
  if (!res.ok) throw new Error(`SnapAPI ${res.status}: ${await res.text()}`);
  return Buffer.from(await res.arrayBuffer());
}

// Save to disk
const png = await screenshot('https://example.com');
await fs.promises.writeFile('example.png', png);
console.log(`Saved: ${png.length} bytes`);

Express.js Screenshot Endpoint

import express from 'express';
const app = express();
const SNAP_KEY = process.env.SNAPAPI_KEY;

app.get('/screenshot', async (req, res) => {
  const { url, width = 1280, height = 800, full_page = 'false' } = req.query;
  if (!url) return res.status(400).json({ error: 'url required' });

  try {
    const params = new URLSearchParams({ access_key: SNAP_KEY, url, width, height, full_page, format: 'png' });
    const upstream = await fetch(`https://api.snapapi.pics/screenshot?${params}`);
    if (!upstream.ok) throw new Error(`upstream ${upstream.status}`);

    res.set('Content-Type', 'image/png');
    res.set('Cache-Control', 'public, max-age=3600');
    upstream.body.pipe(res);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000, () => console.log('Screenshot proxy on :3000'));

AWS Lambda Handler

// handler.mjs — works within 250MB Lambda limit (no Chromium!)
export const handler = async (event) => {
  const { url, width = 1440, height = 900 } = event.queryStringParameters ?? {};
  if (!url) return { statusCode: 400, body: JSON.stringify({ error: 'url required' }) };

  const params = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY,
    url, width, height, format: 'png', full_page: 'false'
  });

  const res = await fetch(`https://api.snapapi.pics/screenshot?${params}`);
  if (!res.ok) return { statusCode: 502, body: JSON.stringify({ error: 'upstream failed' }) };

  const buf = Buffer.from(await res.arrayBuffer());
  return {
    statusCode: 200,
    isBase64Encoded: true,
    headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' },
    body: buf.toString('base64'),
  };
};

Batch Screenshot with Concurrency Control

import { mkdir, writeFile } from 'fs/promises';
import path from 'path';

const SNAP_KEY = process.env.SNAPAPI_KEY;
const CONCURRENCY = 5;

async function batchScreenshot(urls, outputDir = 'screenshots') {
  await mkdir(outputDir, { recursive: true });
  const results = [];
  const queue = [...urls];

  async function worker() {
    while (queue.length > 0) {
      const url = queue.shift();
      const params = new URLSearchParams({ access_key: SNAP_KEY, url, width: 1440, height: 900, format: 'png' });
      try {
        const res = await fetch(`https://api.snapapi.pics/screenshot?${params}`);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const slug = url.replace(/[^a-z0-9]/gi, '_').slice(0, 50);
        const file = path.join(outputDir, `${slug}.png`);
        await writeFile(file, Buffer.from(await res.arrayBuffer()));
        results.push({ url, ok: true, file });
        console.log('OK:', url);
      } catch (e) {
        results.push({ url, ok: false, error: e.message });
        console.error('FAIL:', url, e.message);
      }
    }
  }

  await Promise.all(Array.from({ length: CONCURRENCY }, worker));
  return results;
}

const urls = [
  'https://github.com', 'https://stripe.com', 'https://vercel.com',
  'https://supabase.com', 'https://neon.tech'
];
const results = await batchScreenshot(urls);
console.log(`Done: ${results.filter(r => r.ok).length}/${results.length}`);

When to Still Use Puppeteer

SnapAPI is not a replacement for every Puppeteer use case. If you need to interact with a page — fill forms, click buttons, navigate multi-step flows — you still need a browser automation library. SnapAPI excels at the read-only use cases: capture this URL, extract this data, scrape this rendered HTML. For write operations, browser automation is the right tool.

But for the vast majority of screenshot use cases — page thumbnails, OG images, visual regression, monitoring, report capture — a screenshot API is simpler, cheaper to operate, and more reliable than managing Puppeteer in production.

Get Started Free

Sign up at snapapi.pics/dashboard for 200 free screenshots/month. No credit card, no browser to install. Copy your API key and the Node.js fetch snippet above and you are capturing screenshots in under 5 minutes.

Cloudflare Workers: Screenshots Without Node.js Runtime

Cloudflare Workers use the V8 isolate model — not Node.js. They support the Web Fetch API natively, which means SnapAPI works perfectly with zero adapters. This is one of the scenarios where a screenshot API completely replaces Puppeteer, since headless browsers cannot run in Workers at all.

// Cloudflare Worker — wrangler.toml: SNAPAPI_KEY in [vars]
export default {
  async fetch(request, env) {
    const { searchParams } = new URL(request.url);
    const url = searchParams.get('url');
    if (!url) return new Response('url required', { status: 400 });

    const params = new URLSearchParams({
      access_key: env.SNAPAPI_KEY,
      url, width: '1440', height: '900',
      format: 'jpeg', quality: '85', full_page: 'false',
    });

    const upstream = await fetch(
      `https://api.snapapi.pics/screenshot?${params}`,
      { cf: { cacheEverything: true, cacheTtl: 3600 } }
    );

    return new Response(upstream.body, {
      headers: {
        'Content-Type': 'image/jpeg',
        'Cache-Control': 'public, max-age=3600',
      },
    });
  }
};

Hono.js Screenshot Service

Hono is a fast, edge-compatible web framework that works on Cloudflare Workers, Deno, Bun, and Node.js. Here is a complete screenshot microservice built with Hono:

import { Hono } from 'hono';
import { cache } from 'hono/cache';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

const app = new Hono();
const SNAP_KEY = process.env.SNAPAPI_KEY;

const schema = z.object({
  url:       z.string().url(),
  width:     z.coerce.number().min(320).max(3840).default(1280),
  height:    z.coerce.number().min(200).max(2160).default(800),
  full_page: z.coerce.boolean().default(false),
  format:    z.enum(['png', 'jpeg']).default('jpeg'),
  quality:   z.coerce.number().min(1).max(100).default(85),
});

app.get(
  '/screenshot',
  cache({ cacheName: 'screenshots', cacheControl: 'max-age=3600' }),
  zValidator('query', schema),
  async (c) => {
    const query = c.req.valid('query');
    const params = new URLSearchParams({
      access_key: SNAP_KEY,
      ...Object.fromEntries(Object.entries(query).map(([k, v]) => [k, String(v)])),
    });
    const res = await fetch(`https://api.snapapi.pics/screenshot?${params}`);
    if (!res.ok) return c.json({ error: 'upstream failed' }, 502);
    return new Response(res.body, {
      headers: { 'Content-Type': `image/${query.format}` }
    });
  }
);

export default app;

Migrating an Existing Puppeteer Codebase to SnapAPI

If you already have Puppeteer code, migrating to SnapAPI is mostly mechanical. The core change is replacing browser.newPage() / page.goto() / page.screenshot() chains with a single fetch call. Here is a before/after comparison for the most common patterns:

// BEFORE: Puppeteer
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
const screenshot = await page.screenshot({ type: 'png', fullPage: false });
await browser.close();

// AFTER: SnapAPI (same result, no browser)
const params = new URLSearchParams({
  access_key: process.env.SNAPAPI_KEY,
  url, width: '1280', height: '800',
  format: 'png', full_page: 'false',
});
const res = await fetch(`https://api.snapapi.pics/screenshot?${params}`);
const screenshot = Buffer.from(await res.arrayBuffer());

Benchmark: Puppeteer vs SnapAPI Latency

Cold start is where Puppeteer really hurts. Launching a new Chromium instance takes 800ms-3s depending on your server. SnapAPI's first-request latency is typically 1-4s (browser launch on our infrastructure) with warm subsequent requests under 500ms for simple pages. For serverless functions where you cannot keep a browser warm between invocations, SnapAPI is consistently faster because our browser pool is always warm.

For long-running Node.js servers with a persistent Puppeteer browser pool, the latency comparison is closer — but you are trading engineering effort for marginal latency gains. Most teams find the operational simplicity of SnapAPI worth 100-200ms of additional latency.

Start Capturing Screenshots Today

Get your free API key at snapapi.pics/dashboard. 200 screenshots/month free, no credit card required. The Node.js fetch snippet at the top of this article is everything you need to get started — copy it into your project and you are live in minutes.