Tutorial Mar 15, 2026

How to Take Screenshots in Node.js: Puppeteer vs API Comparison

Take screenshots in Node.js with Puppeteer (500+ lines of setup) or an API (5 lines of code). Which is right for your use case?

You need to take screenshots in your Node.js app. Maybe you're:

You search for "Node.js screenshot" and find Puppeteer. It's open source. It's free. It's got 85k GitHub stars.

Six hours later, you're still debugging Chrome binary paths, memory leaks, and server crashes.

There's a better way. Let me show you both approaches — Puppeteer and a hosted API — so you can decide which fits your needs.

The Puppeteer Approach

Puppeteer is a Node.js library that controls Chrome programmatically. Here's the minimal working example:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'example.png' });
  await browser.close();
})();

That's 9 lines. Looks simple, right?

But this is the honeymoon phase. In production, you'll need:

1. Browser Management

const browser = await puppeteer.launch({
  headless: true,
  args: [
    '--no-sandbox',             // Required on Linux servers
    '--disable-setuid-sandbox',
    '--disable-dev-shm-usage',  // Prevent memory issues
    '--single-process',         // Risk: one crash kills all tabs
  ]
});

2. Error Handling & Timeouts

const browser = await puppeteer.launch({ timeout: 30000 });
const page = await browser.newPage();
page.setDefaultTimeout(10000);
page.setDefaultNavigationTimeout(10000);

try {
  await page.goto(url, { waitUntil: 'networkidle2' });
} catch (error) {
  console.error('Navigation failed:', error);
  // What now? Retry? Log? Alert?
}

3. Memory & Resource Management

// Prevent memory leaks
await page.close();
await browser.close();

// But what if the request times out?
// What if the user disconnects?
// You need try/finally blocks everywhere

4. Concurrency

// You can't reuse the same browser for multiple requests safely
// So you create a pool:

const pool = [];
for (let i = 0; i < 10; i++) {
  pool.push(puppeteer.launch());
}

// Now manage which browser handles which request
// Handle browser crashes gracefully
// Respawn dead browsers
// This is essentially a process manager

5. The Real Cost

Once you have Puppeteer working on your local machine, here's what happens in production:

Real-world Puppeteer cost at 10,000 screenshots/month:

That's before you add features like styled screenshots, device presets, or PDF generation.

The API Approach

Here's the same task with a hosted screenshot API:

const response = await fetch('https://pagebolt.dev/api/v1/screenshot', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://example.com'
  })
});

const buffer = await response.arrayBuffer();
const fs = require('fs');
fs.writeFileSync('screenshot.png', Buffer.from(buffer));

That's it. 13 lines, including imports. No browser management. No memory leaks. No server crashes.

But let's be honest — a 5-line example is boring. Here's what a real implementation looks like:

const express = require('express');

const app = express();
app.use(express.json());

app.post('/api/screenshot', async (req, res) => {
  const { url } = req.body;

  try {
    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,
        height: 720,
        deviceScaleFactor: 2,
        blockAds: true,
        fullPage: false
      })
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    const buffer = await response.arrayBuffer();
    res.setHeader('Content-Type', 'image/png');
    res.send(Buffer.from(buffer));
  } catch (error) {
    console.error('Screenshot failed:', error);
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);

That's 35 lines of real, production-ready code. Compare that to managing Puppeteer.

Feature Comparison

Feature Puppeteer Hosted API
Setup time30 minutes to 6 hours5 minutes
Lines of code200+ (pools, error handling)30–50
InfrastructureYou manage Chrome, memory, scalingHandled for you
Cost (10k/month)$3,500/month$29/month
Device presetsManual code per deviceBuilt-in (25+ presets)
PDF generationExtra setup, more memoryOne parameter
Styled screenshotsDIY with extra toolsBuilt-in (frames, themes)
ReliabilityDepends on your uptime99.9% uptime SLA
MonitoringYou do itIncluded

When to Use Puppeteer

Use Puppeteer if:

When to Use an API

Use an API if:

Real-World Example: Building a Link Preview Service

You're building a service that generates preview cards for shared links (like Twitter/Discord do).

With Puppeteer:

  1. Deploy to a server with enough RAM for concurrent Chrome processes
  2. Build a request queue to manage browser pools
  3. Handle crashes, memory leaks, and zombie processes
  4. Monitor OOM (out of memory) errors
  5. Scale horizontally (more servers = more complexity)
  6. Cost: $1,000–5,000/month in infrastructure

With an API:

  1. Call the API endpoint
  2. Get screenshot + metadata
  3. Generate preview card
  4. Return to user
  5. Cost: $29/month

The API approach takes one week. The Puppeteer approach takes one month (including operational overhead).

Code Example: Link Preview Service

const express = require('express');

const app = express();
app.use(express.json());

app.post('/api/preview', async (req, res) => {
  const { url } = req.body;

  try {
    // Step 1: Take screenshot
    const screenshotResponse = 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: 1200,
        height: 630,
        blockAds: true,
        blockBanners: true
      })
    });

    if (!screenshotResponse.ok) {
      throw new Error('Screenshot failed');
    }

    // Step 2: Inspect page metadata
    const metadataResponse = await fetch('https://pagebolt.dev/api/v1/inspect', {
      method: 'POST',
      headers: {
        'x-api-key': process.env.PAGEBOLT_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ url, extractMetadata: true })
    });

    const metadata = await metadataResponse.json();

    // Step 3: Return preview card data
    const imageBuffer = await screenshotResponse.arrayBuffer();
    res.json({
      title: metadata.title || 'Untitled',
      description: metadata.description || '',
      imageBase64: Buffer.from(imageBuffer).toString('base64'),
      url
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);

That's everything. No browser management. No memory issues. No ops overhead.

Hybrid Approach

Some teams use both:

This gives you the best of both worlds: control where it matters, simplicity where it doesn't.

The Bottom Line

Puppeteer is powerful if you need it. But most teams don't. They need screenshots, and they need them to work reliably without becoming DevOps engineers.

An API costs $29/month and takes 5 minutes to integrate. Puppeteer costs $3,500+/month and 6 weeks to get right.

The choice depends on your constraints. But for most use cases, the API wins on simplicity, cost, and reliability.

Try PageBolt free — 100 requests/month, no credit card

5 lines of code. No Chrome to manage. Works in Node.js, Python, Go, or any HTTP client.

Get free API key