DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Stop maintaining Puppeteer infrastructure: use a screenshot API instead

Stop Maintaining Puppeteer Infrastructure: Use a Screenshot API Instead

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.

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:

  1. You need custom browser behavior — intercepting requests, injecting scripts into specific DOM states, testing interactions
  2. You're already running it — the cost is sunk, the system is stable
  3. You need offline capability — no external API calls, air-gapped environments
  4. 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:

  1. You're spending more time on infrastructure than on your actual product
  2. Screenshot generation is not your core business — it's a reporting feature, a notification attachment, an OG image
  3. You hit concurrency problems — more than one Puppeteer instance at a time gets expensive
  4. 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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' }),
});
Enter fullscreen mode Exit fullscreen mode

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 }),
});
Enter fullscreen mode Exit fullscreen mode

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 }),
});
Enter fullscreen mode Exit fullscreen mode

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 },
    ],
  }),
});
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)