Stop maintaining Puppeteer infrastructure: use a screenshot API instead
Managing Puppeteer in production is expensive: memory leaks, Chromium updates, cold starts, Docker image bloat. Here's when to replace it with a hosted screenshot API.
You added Puppeteer to your project for one reason: generate a screenshot or a PDF. Three months later, your Docker image is 800MB heavier, your CI pipeline takes twice as long, and you've debugged three separate memory leak incidents in production.
Puppeteer is a great tool. But running it in production is infrastructure work — and for many use cases, that work is disproportionate to what you actually need.
This post covers when to keep Puppeteer and when to replace it with a hosted API.
/record_video endpoint. 🔊 Audio on for narration.The actual cost of running Puppeteer in production
Memory
Chromium is a memory hog. A Puppeteer instance handling concurrent requests needs 300–800MB of RAM just for the browser. If you're on a small instance (512MB, 1GB), you're competing with your application for memory.
Common symptoms:
- OOM kills in the middle of requests
- Container restarts under load
- Memory gradually growing until the process dies (browser never fully releases memory after each page visit)
Chromium updates
Puppeteer pins to a specific Chromium version. When sites update their CSP headers, add new Web APIs, or use new CSS features, your screenshots break silently. You won't know until a user complains.
Updating Puppeteer means updating Chromium, which means re-testing all your screenshots, PDF layouts, and any page interactions.
Cold starts
Lambda and Cloud Run aren't ideal for Puppeteer. Chromium takes 2–4 seconds to launch. If you're scaling to zero, every cold start adds latency to your user-facing screenshot generation.
Workarounds (warm pools, persistent containers) add cost and operational complexity.
Docker image size
node:18-slim + Puppeteer + all Chromium dependencies = 600–900MB images. If you build frequently, that's storage cost, bandwidth cost, and slower deploys.
You can use puppeteer-core with a separate Chrome layer, but that's another moving part to maintain.
When to keep Puppeteer
Puppeteer is the right choice when:
- You need custom browser behavior — intercepting requests, injecting scripts into specific DOM states, testing interactions
- You're already running it — the cost is sunk, the system is stable
- You need offline capability — no external API calls, air-gapped environments
- High volume, cost-sensitive — 100,000+ screenshots/month where API pricing becomes significant
If any of these apply, keep Puppeteer.
When to replace it with an API
Replace it when:
- You're spending more time on infrastructure than on your actual product
- Screenshot generation is not your core business — it's a reporting feature, a notification attachment, an OG image
- You hit concurrency problems — more than one Puppeteer instance at a time gets expensive
- Cold starts are killing your latency
The replacement: a hosted screenshot API
A hosted API runs Puppeteer (or its equivalent) for you. You send a URL or HTML, you get an image back. No Chromium, no Docker layers, no memory management.
Before (Puppeteer)
const puppeteer = require('puppeteer');
async function screenshot(url) {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
});
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const buffer = await page.screenshot({ type: 'png', fullPage: true });
return buffer;
} finally {
await browser.close(); // Don't forget this or you leak processes
}
}
This works. But you also need:
- Handle multiple concurrent calls without spawning too many browsers
- Retry logic when Chrome crashes
- Timeout handling when pages hang
- Memory monitoring
- Chromium version pinning in your Dockerfile
After (hosted API)
async function screenshot(url) {
const response = await fetch('https://pagebolt.dev/api/v1/screenshot', {
method: 'POST',
headers: {
'x-api-key': process.env.PAGEBOLT_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
fullPage: true,
blockBanners: true,
blockAds: true,
}),
});
if (!response.ok) throw new Error(`Screenshot failed: ${response.status}`);
return response.buffer(); // PNG buffer, same as before
}
Same output. No Puppeteer dependency. No Chromium in your Docker image.
Migrating specific patterns
Full-page screenshots
// Puppeteer
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.goto(url, { waitUntil: 'networkidle2' });
const screenshot = await page.screenshot({ fullPage: true });
// API
const response = await fetch('https://pagebolt.dev/api/v1/screenshot', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ url, width: 1280, fullPage: true, waitUntil: 'networkidle' }),
});
PDF generation
// Puppeteer
await page.pdf({ path: 'output.pdf', format: 'A4', printBackground: true });
// API
const response = await fetch('https://pagebolt.dev/api/v1/generate_pdf', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ url, format: 'A4', printBackground: true }),
});
HTML to image (OG images, dynamic cards)
// Puppeteer: render HTML string → screenshot
const page = await browser.newPage();
await page.setContent(html);
await page.setViewport({ width: 1200, height: 630 });
const screenshot = await page.screenshot();
// API
const response = await fetch('https://pagebolt.dev/api/v1/screenshot', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ html, width: 1200, height: 630 }),
});
Authenticated page screenshots (login first)
This is where hosted APIs add real value. Puppeteer requires you to manage sessions; hosted APIs with sequence support handle multi-step flows in a single request:
// API: login then screenshot, all in one session
const response = await fetch('https://pagebolt.dev/api/v1/run_sequence', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
steps: [
{ action: 'navigate', url: 'https://app.example.com/login' },
{ action: 'fill', selector: 'input[name="email"]', value: process.env.APP_EMAIL },
{ action: 'fill', selector: 'input[name="password"]', value: process.env.APP_PASSWORD },
{ action: 'click', selector: 'button[type="submit"]' },
{ action: 'wait', ms: 2000 },
{ action: 'navigate', url: 'https://app.example.com/dashboard' },
{ action: 'screenshot', name: 'dashboard', fullPage: true },
],
}),
});
The math on cost
At PageBolt:
- Free: 100 requests/month (no credit card)
- Starter: $29/month for 5,000 requests
- Growth: $79/month for 25,000 requests
Compare that to running your own infrastructure:
| Option | Monthly cost | Maintenance |
|---|---|---|
| Self-hosted Puppeteer on a $20/mo server | $20+ | 2-4 hours/month |
| Lambda with warm pool for cold starts | $30-80 | 3-5 hours/month |
| PageBolt Starter (5,000 requests) | $29 | 0 hours |
For most apps generating under 5,000 screenshots a month, hosted is cheaper when you count engineering time.
What you lose
Be honest about the trade-offs:
- Vendor dependency — if PageBolt is down, your screenshots are down. Mitigate with caching.
- Less flexibility — custom browser extensions, specific Chrome flags, deep DOM control aren't available
- Latency — an external HTTP call adds ~300-800ms vs a local Puppeteer instance (already warmed)
- Data privacy — your URLs are sent to a third party. Not suitable for internal tools with sensitive URLs.
Decision checklist
Replace Puppeteer with a hosted API if:
- Screenshots/PDFs are a secondary feature, not your core product
- You're spending >1 hour/month on Puppeteer maintenance
- Your Docker images are bloated and you want to slim them
- You're running on serverless (Lambda, Cloud Run, Vercel) with cold start issues
- You generate fewer than 25,000 screenshots/month
Keep Puppeteer if:
- You need custom browser automation beyond screenshots
- Volume exceeds 100,000 requests/month (API pricing won't make sense)
- Offline/air-gapped environment required
- You have data privacy requirements that prevent external HTTP calls
Try PageBolt free — 100 requests/month, no credit card. → Get started in 2 minutes
Get Started Free
100 requests/month, no credit card
One API key. Zero browser orchestration overhead in your agent's context window.
Get Your Free API Key →