February 27, 2026 · 6 min read
Puppeteer Screenshot API:
Stop Managing Chrome Instances
You started with page.screenshot() and it worked great locally. Then you deployed it. Now you're dealing with Chrome crashes, zombie processes eating your RAM, Dockerfile nightmares with missing fonts, and that one page that hangs forever because of a rogue service worker.
Sound familiar? Here's the thing: you don't need to run Chrome yourself. A screenshot API does it for you, and your code gets simpler in the process.
The Puppeteer Tax
Running Puppeteer in production means maintaining a surprising amount of infrastructure:
- Memory management - Each Chrome tab uses 50-300MB. Ten concurrent screenshots? That's 3GB of RAM.
- Process cleanup - Chrome zombie processes are real. Your server slowly leaks memory until it crashes at 3 AM.
- Font rendering - Missing system fonts on Linux produce garbled screenshots. You need fonts-liberation, fonts-noto, and half of Google Fonts installed.
- Timeouts and hangs - Some pages never fire load or networkidle. You need custom timeout logic.
- Docker complexity - Your Dockerfile is 40 lines of apt-get install and Chrome flags.
- Security sandbox - Running Chrome with --no-sandbox in Docker because the seccomp profile doesn't work? That's a vulnerability.
The total cost isn't just the VPS. It's the engineer hours you spend debugging Chrome when you should be building your product.
Before and After
Here's what your Puppeteer screenshot code probably looks like:
const puppeteer = require('puppeteer');
async function takeScreenshot(url) {
let browser;
try {
browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--single-process',
],
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30000
});
// Wait for dynamic content
await page.waitForTimeout(2000);
const screenshot = await page.screenshot({
type: 'png',
fullPage: false
});
return screenshot;
} catch (error) {
console.error('Screenshot failed:', error);
throw error;
} finally {
if (browser) await browser.close().catch(() => {});
}
}
Here's the same thing with a screenshot API:
const API_KEY = 'gs_your_api_key';
async function takeScreenshot(url) {
const params = new URLSearchParams({
url,
apiKey: API_KEY,
width: '1280',
height: '800',
format: 'png',
});
const res = await fetch(
`https://grabshot.dev/v1/screenshot?${params}`
);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
return Buffer.from(await res.arrayBuffer());
}
No browser launch. No sandbox flags. No cleanup. No zombie processes. The API handles all of that.
What You Get vs. What You Give Up
| Self-hosted Puppeteer | Screenshot API | |
|---|---|---|
| Setup time | Hours (Docker, fonts, sandbox) | Minutes (get API key, call endpoint) |
| Infra cost | $20-100/mo for a capable VPS | $0-29/mo depending on volume |
| Concurrency | Limited by RAM (3-10 concurrent) | Handled by the service |
| Maintenance | Chrome updates, dependency patches | None |
| Custom JS execution | Full control | Limited (depends on API) |
| Page interaction | Click, type, scroll - anything | Basic (viewport, wait, scroll) |
The trade-off is clear: if you need to click buttons, fill forms, or run complex page interactions before screenshotting, stick with Puppeteer. If you just need to capture how a URL looks, an API is dramatically simpler.
Migration Guide: Puppeteer to GrabShot API
Step 1: Get a free API key at grabshot.dev/dashboard (25 screenshots/month, no credit card).
Step 2: Replace your Puppeteer code. Map your options:
| Puppeteer option | GrabShot parameter |
|---|---|
| page.setViewport({width, height}) | width, height |
| page.screenshot({fullPage: true}) | fullPage=true |
| page.screenshot({type: 'jpeg'}) | format=jpeg |
| page.waitForTimeout(ms) | delay=ms |
| page.emulate(device) | width=375&height=812 (set mobile dimensions) |
Step 3: Remove Puppeteer from your dependencies and your Dockerfile. Enjoy the smaller container image.
When to Keep Puppeteer
A screenshot API isn't always the right choice. Keep Puppeteer if you need to:
- Log in to authenticated pages before screenshotting
- Interact with the page (click buttons, fill forms, navigate)
- Scrape data alongside taking screenshots
- Capture pages on your local network or behind a firewall
- Take hundreds of thousands of screenshots per month (self-hosting may be cheaper at scale)
For everything else - OG image generation, visual regression baselines, website thumbnails, PDF reports with page previews - an API call is simpler, cheaper, and more reliable than managing Chrome.
Quick Examples
cURL (simplest possible):
curl "https://grabshot.dev/v1/screenshot?url=https://github.com&apiKey=YOUR_KEY" \
--output github.png
Python:
import requests
r = requests.get("https://grabshot.dev/v1/screenshot", params={
"url": "https://github.com",
"apiKey": "YOUR_KEY",
"width": 1440,
"format": "webp"
})
with open("github.webp", "wb") as f:
f.write(r.content)
Node.js (with full-page capture):
const params = new URLSearchParams({
url: 'https://github.com',
apiKey: 'YOUR_KEY',
fullPage: 'true',
format: 'png',
});
const res = await fetch(
`https://grabshot.dev/v1/screenshot?${params}`
);
const buffer = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('github-full.png', buffer);
Try it free
GrabShot gives you 25 free screenshots per month. No credit card, no trial expiration. Get your API key and start replacing Puppeteer in under a minute.