Keyword: puppeteer alternative · Published June 8, 2026
Puppeteer Alternative for Screenshots — When API Beats DIY
Puppeteer is a great tool for browser automation. For taking screenshots at scale, it is often the wrong one. This article breaks down the real operational cost of running headless Chrome yourself, what you end up building on top of Puppeteer to make it production-ready, and where a screenshot API replaces all of that with a single HTTP call.
TL;DR
- Keep Puppeteer if screenshots are a small part of a larger automation workflow (form filling, scraping, E2E testing) that already runs in your Node.js process, and volume is under ~1,000/day.
- Switch to an API if taking screenshots is the primary job, volume is growing, you need features like caching / geolocation / ad-blocking, or you're spending engineering time on Chrome lifecycle issues. See how screenshot API pricing works to compare costs.
- Self-host SnapshotFlow if you have data-residency requirements but still want the API interface rather than raw Puppeteer.
The Puppeteer tax
Puppeteer is open-source and free. But running it in production has a tax that compounds with volume:
Memory and Chrome processes
Each headless Chrome instance requires roughly 150–300 MB RAM plus the Node.js process overhead — a typical range reported across Puppeteer community benchmarks and the Puppeteer FAQ. At 5 concurrent screenshots you need 1–2 GB free just for Chrome, before your application uses any memory. On a shared server this competes with your primary workload.
Chrome also leaks memory under sustained load. The standard mitigation is restarting the browser every N requests or on a schedule — which requires a pool manager, a health check, and graceful drain logic. This is several hundred lines of infrastructure code that has nothing to do with your actual product.
Concurrency and queuing
Puppeteer has no built-in concurrency limit. Naïvely firing 50 parallel page.screenshot() calls will exhaust memory and crash the process. You need a queue — typically p-queue or Bull — to cap concurrent jobs, handle backpressure, and surface errors to callers.
Crash recovery
Chrome crashes. Pages time out. Network errors leave browser contexts open. In production you need: a watchdog that detects a hung page and closes it, a circuit breaker that restarts the Chrome process after N consecutive failures, and proper cleanup so zombie contexts don't accumulate.
Missing features you'll need to build
The features below are not in Puppeteer. Each one is a separate library or build-it-yourself project:
| Feature | Puppeteer | SnapshotFlow API |
|---|---|---|
| HTTP response caching (avoid re-rendering same URL) | ❌ Build with Redis / CDN | ✓ Built-in, cache hits free of quota |
| Async job + webhook delivery | ❌ Build with Bull / BullMQ | ✓ async=true + webhook_url |
| Ad / tracker blocking | ❌ page.setRequestInterception() + blocklist | ✓ Ghostery lists, one param |
| Cookie banner blocking | ❌ Custom click automation | ✓ block_cookie_banners=true |
| Geolocation spoofing | ❌ page.setGeolocation() + VPN/proxy | ✓ geolocation_latitude, geolocation_longitude |
| Visual diff / regression | ❌ Capture twice + pixelmatch manually | ✓ GET /diff endpoint with threshold |
| Signed webhook verification | ❌ Build HMAC signing yourself | ✓ HMAC-signed webhook payload |
| Screenshot history + audit log | ❌ Write to your own DB | ✓ GET /screenshots paginated |
| Batch (multiple URLs in one call) | ❌ Promise.all with your own limiter | ✓ POST /batch up to 10 URLs |
| MCP tool (AI agent integration) | ❌ Wrap yourself | ✓ Remote MCP server with OAuth 2.0 |
Real cost comparison
The comparison that matters is not "free open-source vs paid API." It's total cost of ownership at different volumes.
| Volume | DIY Puppeteer (AWS) | SnapshotFlow API |
|---|---|---|
| ≤ 200 / month | ~$10–15/mo (t3.small, shared) + your time to set up | $0 — free tier (200 screenshots/mo, no card) |
| ~2,000 / month | ~$30/mo (t3.medium, 2 vCPU / 4 GB) + queue maintenance | Paid tier (pricing rolling out — roughly comparable to hosted competition) |
| ~50,000 / month | ~$120–200/mo (2× c6i.large) + DevOps hours for auto-scaling, crash monitoring | API pricing at volume; or self-host on your own infra for fixed cost |
| 1M+ / month | Dedicated Chrome cluster, auto-scaling group, ops team involvement | Self-host SnapshotFlow on your own infra — Docker Compose, Redis, your hardware |
AWS on-demand pricing as of June 2026 (aws.amazon.com/ec2/pricing). t3.small: $0.0208/hr (~$15/mo). t3.medium: $0.0416/hr (~$30/mo). c6i.large: $0.085/hr (~$62/mo each). Chrome RAM estimates based on Puppeteer's documented 150–300 MB per page context. DIY cost excludes engineering time.
The crossover point where DIY becomes cheaper than a hosted API is typically above 1M screenshots/month at sustained load — and only if you already have infrastructure engineers on hand. Below that threshold the API wins on engineer-hours even when the raw compute cost is slightly higher.
Code: Puppeteer → SnapshotFlow
A minimal Puppeteer screenshot in Node.js looks like this. It works fine — until you need concurrency, caching, or any of the features in the table above.
Puppeteer (Node.js)
import puppeteer from 'puppeteer';
// Launch Chrome — expensive, do this once and reuse
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
});
async function takeScreenshot(url) {
const page = await browser.newPage();
try {
await page.setViewport({ width: 1280, height: 800 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
const buffer = await page.screenshot({ fullPage: true, type: 'png' });
return buffer;
} finally {
await page.close(); // always close — zombie pages accumulate otherwise
}
}
// Usage
const png = await takeScreenshot('https://example.com');
That's the happy path. In production you also need: a concurrency limiter, a retry wrapper, crash detection on browser.on('disconnected', ...), and cleanup logic. Here's the same outcome with the API:
SnapshotFlow API (Node.js)
// No browser to launch. No process to manage.
const API_KEY = process.env.SNAPSHOTFLOW_API_KEY;
async function takeScreenshot(url) {
const params = new URLSearchParams({
url,
full_page: 'true',
format: 'png',
viewport_width: '1280',
viewport_height: '800',
block_ads: 'true',
block_cookie_banners: 'true',
});
const res = await fetch(`https://api.snapshotflow.com/screenshot?${params}`, {
headers: { 'X-Api-Key': API_KEY },
});
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
return Buffer.from(await res.arrayBuffer());
}
// Usage — identical output, zero infrastructure
const png = await takeScreenshot('https://example.com');
Python
import os, httpx
API_KEY = os.environ["SNAPSHOTFLOW_API_KEY"]
def take_screenshot(url: str) -> bytes:
r = httpx.get(
"https://api.snapshotflow.com/screenshot",
params={
"url": url,
"full_page": "true",
"format": "png",
"viewport_width": "1280",
"block_ads": "true",
"block_cookie_banners": "true",
},
headers={"X-Api-Key": API_KEY},
timeout=30,
)
r.raise_for_status()
return r.content
png = take_screenshot("https://example.com")
Async + webhook (fire-and-forget)
For high-volume jobs you don't want to block on the HTTP response. Pass async=true and a webhook_url — the API returns a job ID immediately and POSTs the result to your endpoint when done.
const res = await fetch('https://api.snapshotflow.com/screenshot', {
method: 'POST',
headers: {
'X-Api-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com',
full_page: true,
format: 'png',
async: true,
webhook_url: 'https://yourapp.com/webhooks/screenshot',
}),
});
const { job_id } = await res.json();
console.log('Job queued:', job_id);
// Your webhook receives the PNG URL when rendering is complete
Migration guide — Puppeteer options → API params
The most common Puppeteer screenshot options map directly to SnapshotFlow query parameters.
| Puppeteer option | SnapshotFlow param | Notes |
|---|---|---|
page.setViewport({ width, height }) | viewport_width, viewport_height | Same semantics, CSS pixels |
page.setViewport({ deviceScaleFactor }) | device_scale_factor | Default 1; use 2 for Retina |
screenshot({ fullPage: true }) | full_page=true | Captures full scroll height |
screenshot({ type: 'jpeg', quality: 80 }) | format=jpg&image_quality=80 | |
screenshot({ clip: { x, y, width, height } }) | clip_x, clip_y, clip_width, clip_height | |
page.waitForSelector('.hero') | wait_for_selector=.hero | |
page.waitForTimeout(2000) | delay=2000 | ms; max 10,000 |
page.goto(url, { waitUntil: 'networkidle2' }) | wait_until=networkidle2 | Accepts load, domcontentloaded, networkidle0, networkidle2 |
page.setExtraHTTPHeaders(headers) | headers (JSON string) | |
page.setCookie(...) | cookies (JSON string) | |
page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]) | dark_mode=true | |
page.setGeolocation({ latitude, longitude }) | geolocation_latitude, geolocation_longitude | |
page.addStyleTag({ content: css }) | styles | Raw CSS string |
page.addScriptTag({ content: js }) | scripts | Raw JS string, runs before capture |
page.pdf({ format: 'A4' }) | format=pdf&pdf_format=A4 | Also supports A3, Letter, Legal, Tabloid |
When to keep Puppeteer
A screenshot API doesn't replace Puppeteer for every use case. Keep Puppeteer when you need:
- Full browser automation — form filling, multi-step login flows, drag-and-drop interactions, file upload testing. An API takes one screenshot; it doesn't drive a user session.
- End-to-end test suites — Puppeteer integrates with testing frameworks (Jest, Mocha) for behavioral assertions alongside visual captures. Use Playwright or Puppeteer for this, and call the screenshot API only for the visual regression step.
- Custom Chrome extensions or profiles — if you need to inject a browser extension, use a specific Chrome profile, or intercept every network request at the socket level, you need direct browser control.
- Very low volume (< 200 screenshots/month) — at this volume the SnapshotFlow free tier is actually the better deal, but if you already have Puppeteer running in an existing Node process there's no compelling reason to swap it out.
Need on-prem? Self-host SnapshotFlow
Data-residency requirements are the most common reason teams run Puppeteer instead of a hosted API. SnapshotFlow addresses this with a self-hosted option: a Dockerfile and docker-compose.yml that run the full API (Chrome, Redis, storage) inside your own network.
# Clone the backend repo and start
git clone https://github.com/snapshotflow/snapshotflow-backend
cd snapshotflow-backend
cp .env.example .env # set AUTH_ENABLED, STORAGE_TYPE, REDIS_URL
docker compose up -d
# Your local API is now at http://localhost:3000/screenshot
curl "http://localhost:3000/screenshot?url=https://example.com&full_page=true" \
-H "X-Api-Key: your_local_key" \
--output screenshot.png
You get the same HTTP interface as the hosted service — all the params from the migration table above — without any data leaving your infrastructure. Self-hosting gives you the API ergonomics without the third-party dependency.
FAQ
When should I use a screenshot API instead of Puppeteer?
Use an API when screenshots are not your core product: you need caching, geolocation, ad-blocking, retries, or async delivery without building them yourself; you want to avoid managing Chrome processes and memory; or you need more than ~5 concurrent captures without provisioning extra servers.
Is Puppeteer free? What does it actually cost to run?
Puppeteer itself is open-source, but the server is not free. A minimal Node.js + Chrome instance needs at least 1 GB RAM. At AWS t3.medium pricing (~$30/month) you can reliably handle 3–5 concurrent screenshots. At higher volume you need a queue, auto-scaling, and Chrome pool management — which adds engineering time on top of infrastructure cost.
Can I replace Puppeteer with SnapshotFlow?
Yes, for screenshot use cases. Replace the Puppeteer block with a single fetch() call to api.snapshotflow.com/screenshot. The API handles Chrome lifecycle, caching, retries, and async delivery. See the parameter migration table above.
Does SnapshotFlow support full-page screenshots like Puppeteer?
Yes. Pass full_page=true to capture the full scrollable height — the same as Puppeteer's fullPage: true. You can also set viewport_width, viewport_height, and device_scale_factor to match any device profile.
What if I need to keep data on-premise?
SnapshotFlow ships a Dockerfile and docker-compose.yml for self-hosting in your own VPC. You get the full API feature set — full page, PDF, async, caching, ad blocking — without sending URLs to a third-party host.
Does the API support JavaScript-rendered pages?
Yes. The API runs real Chromium, so JavaScript executes fully before capture. Use wait_until=networkidle2 or wait_for_selector=.your-element to ensure dynamic content has loaded, exactly like Puppeteer's waitUntil and waitForSelector.
Try it — 200 free screenshots, no card
The free tier gives you 200 screenshots per month for the lifetime of the account with no credit card required. Self-host is available for teams that need data to stay inside their network. If your Puppeteer setup is giving you grief, the migration is a base-URL swap and a parameter rename — see the table above.