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.

Puppeteer DIY vs SnapshotFlow API — stack comparison Two columns showing what you maintain with Puppeteer vs what SnapshotFlow handles for you. What you maintain — DIY Puppeteer vs Screenshot API Every item on the left is your responsibility. Every item on the right is handled by the API. DIY Puppeteer Chrome / Chromium binary + OS dependencies Process pool + concurrency limiter Memory leak detection + crash recovery Job queue + retry logic Caching layer (Redis / S3) Ad / cookie banner blocking, geolocation SnapshotFlow API One HTTP call — GET /screenshot?url=… Concurrency managed server-side Auto-restart, health checks built-in Async + webhook, automatic retries HTTP cache — cache hits free of quota Ghostery ad blocking, geolocation params
Figure 1. Everything in red is your engineering and operational responsibility with DIY Puppeteer. Everything in green is already solved by the API.

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 core trade-off: Puppeteer gives you full browser control in exchange for full operational responsibility. A screenshot API gives you a simple HTTP interface in exchange for giving up low-level browser hooks. Most teams taking screenshots at scale find the trade worth it — the cost comparison below shows why.

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.

Real-world signal: the most common GitHub issues on Puppeteer repos are about memory leaks, zombie Chrome processes, and "Target closed" errors under load. These are solved problems — but you solve them yourself, or you pay an API to solve them for you.

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:

FeaturePuppeteerSnapshotFlow 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 / BullMQasync=true + webhook_url
Ad / tracker blockingpage.setRequestInterception() + blocklist✓ Ghostery lists, one param
Cookie banner blocking❌ Custom click automationblock_cookie_banners=true
Geolocation spoofingpage.setGeolocation() + VPN/proxygeolocation_latitude, geolocation_longitude
Visual diff / regression❌ Capture twice + pixelmatch manuallyGET /diff endpoint with threshold
Signed webhook verification❌ Build HMAC signing yourself✓ HMAC-signed webhook payload
Screenshot history + audit log❌ Write to your own DBGET /screenshots paginated
Batch (multiple URLs in one call)❌ Promise.all with your own limiterPOST /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.

VolumeDIY 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 optionSnapshotFlow paramNotes
page.setViewport({ width, height })viewport_width, viewport_heightSame semantics, CSS pixels
page.setViewport({ deviceScaleFactor })device_scale_factorDefault 1; use 2 for Retina
screenshot({ fullPage: true })full_page=trueCaptures 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=2000ms; max 10,000
page.goto(url, { waitUntil: 'networkidle2' })wait_until=networkidle2Accepts 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 })stylesRaw CSS string
page.addScriptTag({ content: js })scriptsRaw JS string, runs before capture
page.pdf({ format: 'A4' })format=pdf&pdf_format=A4Also 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.