Keyword: playwright alternative · Published June 10, 2026
Playwright Alternative for Screenshots — When API Beats DIY
Playwright is the best browser automation framework of its generation — for testing. But a lot of teams end up running it in production for a job it was never designed for: turning URLs into images. This article draws the line between the two, breaks down what self-managed browsers really cost once screenshots become a product feature, and shows the migration to a screenshot API — including the part none of the classic comparisons cover: what your AI agents should call.
TL;DR
- Keep Playwright for E2E tests and visual assertions inside your test suite —
expect(page).toHaveScreenshot()is exactly what it's for, and nothing replaces the trace viewer when a test fails. - Switch to a screenshot API the moment screenshots become a runtime feature: link previews, thumbnails, OG images, archiving, monitoring, or images for AI pipelines. Production Playwright means browser binaries on every host, version lock, queue logic, and crash recovery — a single HTTP call replaces all of it.
- For AI agents, the split is the same: Playwright MCP when the agent must drive a browser; a remote screenshot MCP tool when it just needs the image. SnapshotFlow exposes both
screenshotandvisual_difftools at its MCP endpoint — no local browser required.
Playwright is a test tool. Screenshots-as-a-service is a different job
Most "Playwright alternative" articles compare it to Puppeteer, Selenium, or Cypress. That's the wrong axis if your problem is screenshots. Playwright's killer features — auto-waiting, multi-browser engines (Chromium, Firefox, WebKit), codegen, the trace viewer, test sharding — exist to make tests reliable. None of them make production screenshot serving easier.
The trouble starts when a screenshot stops being an assertion and becomes a feature. A user pastes a URL and your app shows a preview. A cron job archives competitor pricing pages nightly. Your CMS generates an OG image per article. Now Playwright isn't running for 90 seconds in CI — it's a long-lived service with uptime expectations, and you've quietly become the operator of a browser farm.
tests/, you're fine. If it lives in src/services/ behind an Express route or a queue worker, you've built an unmanaged screenshot API — and this article is about replacing it with a managed one.
Cost at different volumes
"Playwright is free" is true the way a free puppy is free. The honest comparison is total cost of ownership — compute plus the engineer-hours that keep the browser fleet alive.
| Volume | Self-managed Playwright | SnapshotFlow API |
|---|---|---|
| ≤ 300 total | ~$10–15/mo small instance + setup time + 2 GB image in your registry | $0 — free tier (300 lifetime screenshots, no card) |
| ~2,000 / month | ~$30/mo (2 vCPU / 4 GB) + queue and update maintenance | Paid tier — see how screenshot API pricing works |
| ~50,000 / month | ~$120–200/mo across instances + DevOps hours for scaling and crash monitoring | API at volume; cache hits don't count against quota |
| 1M+ / month | Dedicated browser cluster, auto-scaling, on-call rotation | Self-host SnapshotFlow on your own infra — fixed cost, API ergonomics |
Indicative AWS on-demand pricing, June 2026 (aws.amazon.com/ec2/pricing). Memory estimates assume Chromium-only installs; multi-engine setups cost more. DIY column excludes engineering time, which usually dominates.
Below roughly a million captures per month, the API wins on engineer-hours even when raw compute looks comparable. Above it, self-hosting the API gives you fixed costs without giving up the HTTP interface.
Code: Playwright → SnapshotFlow
A typical production Playwright capture in Node.js — already with the reuse-the-browser optimization most teams add after their first OOM:
Playwright (Node.js)
import { chromium } from 'playwright';
// Launch once, reuse — cold launch costs seconds
const browser = await chromium.launch({
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});
async function takeScreenshot(url) {
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
});
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
return await page.screenshot({ fullPage: true, type: 'png' });
} finally {
await context.close(); // leak contexts and RAM disappears
}
}
const png = await takeScreenshot('https://example.com');
Add the queue, retries, crash watchdog, and Docker image with browsers baked in — none of which is shown above — and compare with the API version:
SnapshotFlow API (Node.js)
// No browsers installed. No contexts to leak. No image to rebuild.
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());
}
const png = await takeScreenshot('https://example.com');
Python — replacing sync_playwright
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")
High volume: async + webhook
Where Playwright needs BullMQ, the API takes async=true and a webhook_url — you get a job ID instantly and an HMAC-signed POST when the render is 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();
Migration cheat sheet — Playwright options → API params
Everything you set on a Playwright page or context maps to a request parameter:
| Playwright option | SnapshotFlow param | Notes |
|---|---|---|
newContext({ viewport: { width, height } }) | viewport_width, viewport_height | CSS pixels, same semantics |
newContext({ deviceScaleFactor: 2 }) | device_scale_factor=2 | Retina output |
screenshot({ fullPage: true }) | full_page=true | Full scroll height — see full-page guide |
screenshot({ type: 'jpeg', quality: 80 }) | format=jpg&image_quality=80 | Also png, webp, pdf |
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 |
goto(url, { waitUntil: 'networkidle' }) | wait_until=networkidle0 | Also load, domcontentloaded, networkidle2 |
newContext({ extraHTTPHeaders }) | headers (JSON string) | |
context.addCookies([...]) | cookies (JSON string) | Authenticated pages |
page.emulateMedia({ colorScheme: 'dark' }) | dark_mode=true | |
context.setGeolocation({ latitude, longitude }) | geolocation_latitude, geolocation_longitude | See geo-targeted screenshots |
page.addStyleTag({ content: css }) | styles | Raw CSS string |
page.addScriptTag({ content: js }) | scripts | Runs before capture |
page.pdf({ format: 'A4' }) | format=pdf&pdf_format=A4 | A3, A4, Letter, Legal, Tabloid |
The part nobody compares: what should your AI agent call?
In 2026 a growing share of screenshots aren't requested by humans or cron jobs — they're requested by agents. Claude reviewing a deployed page, an SEO agent auditing SERP competitors, a support bot attaching "here's what your page looks like" to a ticket. The Playwright-vs-API question now has an agent-shaped version, and the answer follows the same logic.
Playwright MCP gives an agent a real browser session: navigate, click, type, read the accessibility tree. It's the right tool when the agent must interact — log in, fill a form, walk a flow. The cost: a local browser install wherever the agent runs, plus session state the agent has to manage across many tool calls.
A remote screenshot MCP tool is the right tool when the agent just needs the rendered result. One tool call, one image back, nothing installed. SnapshotFlow's remote MCP server (https://api.snapshotflow.com/mcp, OAuth 2.0) exposes screenshot, visual_diff, and extract_text — so an agent can capture a page, diff it against yesterday's capture, or pull the text without spending vision tokens. For the browser-side counterpart of this story, see our WebMCP explainer.
| Playwright MCP | SnapshotFlow MCP | |
|---|---|---|
| Best for | Driving a session: click, type, navigate | Getting pixels: capture, diff, extract |
| Install footprint | Node + browser binaries on the agent host | None — remote server, OAuth 2.0 |
| Tool calls per screenshot | Several (navigate → wait → capture) | One |
| State the agent manages | Browser session, open pages | None — stateless calls |
| Caching, ad blocking, geo | Agent's problem | Server-side params |
The two compose well: agents that browse with Playwright MCP can still call visual_diff on SnapshotFlow for regression checks, because pixel comparison over a stateless API is cheaper than holding two browser sessions open.
When to keep Playwright
A screenshot API replaces Playwright-as-a-screenshot-service, not Playwright. Keep it when you need:
- E2E test suites — auto-waiting locators,
expect(page).toHaveScreenshot(), trace viewer, CI sharding. This is Playwright's home turf and an API doesn't compete there. - Cross-engine rendering checks — if you must verify pages in Firefox and WebKit, you need Playwright's bundled engines; screenshot APIs render in Chromium.
- Multi-step interactions before capture — login flows, wizards, drag-and-drop states. An API can pass cookies and run scripts, but it won't walk a five-step flow for you.
- Already-running infrastructure at huge scale — if you have a tuned browser cluster and the team to run it, migration urgency is low. Compare against self-hosting the API when the maintenance bill comes due.
Need on-prem? Self-host SnapshotFlow
Data residency is the strongest legitimate reason to keep browsers in-house — and it doesn't require keeping raw Playwright in-house. The SnapshotFlow backend ships a Dockerfile and docker-compose.yml that run the entire API — Chrome, Redis, storage, caching, async webhooks — 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
# Same interface as the hosted API
curl "http://localhost:3000/screenshot?url=https://example.com&full_page=true" \
-H "X-Api-Key: your_local_key" \
--output screenshot.png
You keep every parameter from the migration table, your URLs never leave your VPC, and you still don't write queue or crash-recovery code.
FAQ
When should I use a screenshot API instead of Playwright?
When the screenshot is a product feature rather than a test assertion: link previews, thumbnails, OG images, archiving, monitoring, or images for AI pipelines. Keep Playwright when the capture happens inside an E2E test run.
Is Playwright free? What does running it for screenshots actually cost?
The library is free; the operation isn't. You pay for servers, browser binaries reinstalled on every deploy (or a 2 GB+ Docker image), CI minutes, and engineering time for queues, retries, and roughly-monthly browser updates.
Can I replace Playwright with SnapshotFlow?
For capture use cases, yes — swap the chromium.launch() block for one request to api.snapshotflow.com/screenshot using the parameter mapping above. Keep Playwright for your test suite.
Isn't Playwright MCP enough for AI agents?
It's the right choice when the agent must drive a browser. If it only needs a rendered image, a remote MCP tool is one stateless call with no local browser: SnapshotFlow exposes screenshot, visual_diff, and extract_text at https://api.snapshotflow.com/mcp.
Does the API support waitForSelector, custom viewports, dark mode?
Yes — wait_for_selector, wait_until, viewport_width/height, device_scale_factor, dark_mode, headers and cookies are all request parameters with the same semantics as Playwright's options.
What if my data can't leave our infrastructure?
Self-host: the backend ships Dockerfile + docker-compose.yml running the full API in your VPC — same parameters, no third-party host involved.
Try it — 300 free screenshots, no card
The free tier gives you 300 screenshots for the lifetime of the account, no credit card required. If your Playwright workers exist only to turn URLs into images, the migration is a base-URL swap and a parameter rename — the cheat sheet above covers every option you're using today.