Tools: How to Take Screenshots in Node.js: Puppeteer vs API Comparison
The Puppeteer Approach
1. Browser Management
2. Error Handling & Timeouts
4. Concurrency
5. The Real Cost
The API Approach
Feature Comparison
When to Use Puppeteer
When to Use an API
Real-World Example: Building a Link Preview Service
Code Example: Link Preview Service
Hybrid Approach
The Bottom Line
Try PageBolt Free 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. Puppeteer is a Node.js library that controls Chrome programmatically. Here's the minimal working example: That's 9 lines. Looks simple, right? But this is the honeymoon phase. In production, you'll need: 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. Here's the same task with a hosted screenshot API: That's it. 13 lines, including imports and error handling. 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: That's 35 lines of real, production-ready code. Compare that to managing Puppeteer. You're building a service that generates preview cards for shared links (like Twitter/Discord do). The API approach takes one week. The Puppeteer approach takes one month (including operational overhead). That's everything. No browser management. No memory issues. No ops overhead. This gives you the best of both worlds: control where it matters, simplicity where it doesn't. 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. 100 requests/month. No credit card. No setup required. Start your free trial and see how simple screenshots can be. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
$ 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();
})();
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();
})();
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();
})();
const browser = await puppeteer.launch({ headless: true, args: [ '--no-sandbox', // Required on Linux servers '---weight: 500;">disable-setuid-sandbox', '---weight: 500;">disable-dev-shm-usage', // Prevent memory issues '--single-process', // Risk: one crash kills all tabs ]
});
const browser = await puppeteer.launch({ headless: true, args: [ '--no-sandbox', // Required on Linux servers '---weight: 500;">disable-setuid-sandbox', '---weight: 500;">disable-dev-shm-usage', // Prevent memory issues '--single-process', // Risk: one crash kills all tabs ]
});
const browser = await puppeteer.launch({ headless: true, args: [ '--no-sandbox', // Required on Linux servers '---weight: 500;">disable-setuid-sandbox', '---weight: 500;">disable-dev-shm-usage', // Prevent memory issues '--single-process', // Risk: one crash kills all tabs ]
});
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?
}
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?
}
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?
}
// 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
// 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
// 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
// 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
// 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
// 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
const response = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': 'Bearer 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));
const response = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': 'Bearer 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));
const response = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': 'Bearer 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));
const express = require('express');
const fetch = require('node-fetch');
const fs = require('fs'); app.post('/api/screenshot', async (req, res) => { const { url } = req.body; try { const response = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${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.-weight: 500;">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.-weight: 500;">status(500).json({ error: error.message }); }
});
const express = require('express');
const fetch = require('node-fetch');
const fs = require('fs'); app.post('/api/screenshot', async (req, res) => { const { url } = req.body; try { const response = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${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.-weight: 500;">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.-weight: 500;">status(500).json({ error: error.message }); }
});
const express = require('express');
const fetch = require('node-fetch');
const fs = require('fs'); app.post('/api/screenshot', async (req, res) => { const { url } = req.body; try { const response = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${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.-weight: 500;">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.-weight: 500;">status(500).json({ error: error.message }); }
});
const express = require('express');
const fetch = require('node-fetch'); const app = express(); app.post('/api/preview', async (req, res) => { const { url } = req.body; try { // Step 1: Take screenshot const screenshotResponse = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${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: Get page metadata (if using inspect endpoint) const metadataResponse = await fetch('https://api.pagebolt.io/api/v1/inspect', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); const metadata = await metadataResponse.json(); // Step 3: Return preview card res.json({ title: metadata.title || 'Untitled', description: metadata.description || '', image: screenshotResponse.url, // Returns CDN URL url }); } catch (error) { res.-weight: 500;">status(500).json({ error: error.message }); }
}); app.listen(3000);
const express = require('express');
const fetch = require('node-fetch'); const app = express(); app.post('/api/preview', async (req, res) => { const { url } = req.body; try { // Step 1: Take screenshot const screenshotResponse = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${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: Get page metadata (if using inspect endpoint) const metadataResponse = await fetch('https://api.pagebolt.io/api/v1/inspect', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); const metadata = await metadataResponse.json(); // Step 3: Return preview card res.json({ title: metadata.title || 'Untitled', description: metadata.description || '', image: screenshotResponse.url, // Returns CDN URL url }); } catch (error) { res.-weight: 500;">status(500).json({ error: error.message }); }
}); app.listen(3000);
const express = require('express');
const fetch = require('node-fetch'); const app = express(); app.post('/api/preview', async (req, res) => { const { url } = req.body; try { // Step 1: Take screenshot const screenshotResponse = await fetch('https://api.pagebolt.io/api/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${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: Get page metadata (if using inspect endpoint) const metadataResponse = await fetch('https://api.pagebolt.io/api/v1/inspect', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); const metadata = await metadataResponse.json(); // Step 3: Return preview card res.json({ title: metadata.title || 'Untitled', description: metadata.description || '', image: screenshotResponse.url, // Returns CDN URL url }); } catch (error) { res.-weight: 500;">status(500).json({ error: error.message }); }
}); app.listen(3000); - Building a link preview -weight: 500;">service
- Auto-generating OG images for social sharing
- Testing your website across devices
- Archiving web pages for compliance
- Monitoring competitor pricing pages - 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 -weight: 500;">service.
- Reliability: If your server goes down, your screenshot -weight: 500;">service is down. No redundancy. No failover. - Infrastructure: $500/month (dedicated server, 4GB RAM)
- Operational labor: $3,000/month (on-call, monitoring, incident response)
- Total: $3,500/month - 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 - 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 - 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 - Call the API endpoint
- Get screenshot + metadata
- Generate preview card
- Return to user
- Cost: $29/month - Puppeteer for one-off, internal tools (admin dashboards, report generation)
- API for customer-facing features (link previews, screenshot galleries, monitoring)