You need to take screenshots in your Node.js app. Maybe you're:
- Building a link preview service
- Auto-generating OG images for social sharing
- Testing your website across devices
- Archiving web pages for compliance
- Monitoring competitor pricing pages
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:
- Server costs: A single screenshot takes 200–500MB of RAM. With 10 concurrent users, you need 2–5GB of server memory. Add a load balancer, horizontal scaling, and you're looking at $1,000+/month just for the infrastructure.
- Maintenance: Chrome updates break your setup. Your Node version matters. Your server OS matters. You become a DevOps engineer.
- Monitoring: You need to watch for zombie Chrome processes, memory leaks, crashed browsers. One bad website (infinite JavaScript, memory bomb) crashes the entire service.
- Reliability: If your server goes down, your screenshot service is down. No redundancy. No failover.
Real-world Puppeteer cost at 10,000 screenshots/month:
- Infrastructure: $500/month (dedicated server, 4GB RAM)
- Operational labor: $3,000/month (on-call, monitoring, incident response)
- Total: $3,500/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 time | 30 minutes to 6 hours | 5 minutes |
| Lines of code | 200+ (pools, error handling) | 30–50 |
| Infrastructure | You manage Chrome, memory, scaling | Handled for you |
| Cost (10k/month) | $3,500/month | $29/month |
| Device presets | Manual code per device | Built-in (25+ presets) |
| PDF generation | Extra setup, more memory | One parameter |
| Styled screenshots | DIY with extra tools | Built-in (frames, themes) |
| Reliability | Depends on your uptime | 99.9% uptime SLA |
| Monitoring | You do it | Included |
When to Use Puppeteer
Use Puppeteer if:
- You're taking screenshots once per month and can afford downtime
- You have a small, predictable load (< 100 screenshots/month)
- You need to screenshot internal applications behind firewalls (API can't reach them)
- You're learning about browser automation (educational context)
- You have DevOps infrastructure already and want full control
When to Use an API
Use an API if:
- You want screenshots in production without infrastructure headaches
- You need reliability and uptime guarantees
- You want features like device presets, PDF generation, or styled screenshots
- You're scaling (100+ screenshots/month)
- You want to focus on your app, not on browser management
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:
- Deploy to a server with enough RAM for concurrent Chrome processes
- Build a request queue to manage browser pools
- Handle crashes, memory leaks, and zombie processes
- Monitor OOM (out of memory) errors
- Scale horizontally (more servers = more complexity)
- Cost: $1,000–5,000/month in infrastructure
With an API:
- Call the API endpoint
- Get screenshot + metadata
- Generate preview card
- Return to user
- 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:
- Puppeteer for one-off, internal tools (admin dashboards, report generation)
- API for customer-facing features (link previews, screenshot galleries, monitoring)
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.