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.