You're running Puppeteer in production. It works. But every month, you're spending $3,500+ on infrastructure, your Docker images weigh 1.2GB, and your team is debugging browser pool exhaustion at 2 AM.
You're not alone. Thousands of teams have been there. And more are quietly switching to hosted screenshot APIs. Here's why—and how to make the move.
The Hidden Cost of Puppeteer
Puppeteer solves a real problem: headless browser automation at scale. But running it in production means:
Infrastructure costs:
- Chrome instances consume 300–500MB each. At scale (10 concurrent instances), that's 5GB baseline.
- AWS t3.xlarge instance (4 vCPU, 16GB RAM) runs $130/month. With CPU overhead, you need 2–3 of them: $260–390/month.
- Add load balancing, monitoring, backups: $500+/month baseline.
- At 10,000 screenshots/month, that's $0.05 per screenshot (before scaling).
Operational overhead:
- Browser pool management (connection limits, timeouts, retry logic)
- Memory leak monitoring and instance recycling
- Chrome version updates and compatibility testing
- Error handling for half-loaded pages, timeouts, rendering quirks
- Docker image builds taking 8+ minutes
Hidden code complexity:
Here's production-grade Puppeteer code for just taking a single screenshot with error handling and pooling:
const puppeteer = require('puppeteer');
class ScreenshotPool {
constructor(maxConcurrent = 5) {
this.maxConcurrent = maxConcurrent;
this.browsers = [];
this.queue = [];
this.activeCount = 0;
}
async initialize() {
for (let i = 0; i < this.maxConcurrent; i++) {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
});
this.browsers.push({ browser, inUse: false });
}
}
async screenshot(url) {
return new Promise((resolve, reject) => {
this.queue.push({ url, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) return;
const { url, resolve, reject } = this.queue.shift();
this.activeCount++;
try {
const browser = await this.getBrowser();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2', timeout: 10000 });
const buffer = await page.screenshot({ type: 'png' });
await page.close();
resolve(buffer);
} catch (error) {
reject(new Error(`Screenshot failed: ${error.message}`));
} finally {
this.activeCount--;
this.processQueue();
}
}
async getBrowser() {
const available = this.browsers.find(b => !b.inUse);
if (available) {
available.inUse = true;
return available.browser;
}
await new Promise(resolve => setTimeout(resolve, 100));
return this.getBrowser();
}
async close() {
for (const { browser } of this.browsers) {
await browser.close();
}
}
}
module.exports = ScreenshotPool;
That's 60+ lines for one feature. And you still need error recovery, logging, monitoring, and rate limiting. Real-world codebases hit 500+ lines.
The PageBolt Alternative
Same output, 5 lines:
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 })
});
const buffer = Buffer.from(await response.arrayBuffer());
Cost at scale:
- 10,000 screenshots/month: $29.
- 100,000 screenshots/month: $99.
- 1,000,000 screenshots/month: $299.
At 10k screenshots/month, you're saving $471/month on infrastructure alone. Add ops time, and the savings compound.
Real Numbers: A Case Study
| Before (Puppeteer) | After (PageBolt) | |
|---|---|---|
| Infrastructure | $390/month (EC2 + load balancer) | $0 |
| Monitoring & logging | $80/month | $10/month (basic) |
| Ops time (5h/month) | $250/month | ~$0/month |
| CI/CD builds | $30/month (8-min builds) | $0 (90-second builds) |
| API cost | — | $29/month |
| Total | $750/month | $39/month |
Savings: $711/month (95% reduction)
That's not including the 8-minute Docker build time you eliminate—PageBolt requires no Chrome in your container, dropping image size from 1.2GB to ~150MB.
When to Stay with Puppeteer
Puppeteer makes sense if:
- You need extreme customization (injecting scripts, setting cookies before navigation, custom rendering logic)
- You have regulatory requirements for data residency (no API calls allowed)
- You're already running a self-hosted infrastructure and want to squeeze it
For everyone else: screenshot APIs win on cost, simplicity, and operational burden.
The Migration Checklist
- Audit your Puppeteer usage — which endpoints use it? Volume? Error rates?
- Run a parallel test — send 1% of traffic to PageBolt, 99% to Puppeteer. Compare error rates, latency, screenshot quality.
- Set up caching — store screenshots in S3/Redis to avoid redundant API calls.
- Switch gradually — migrate one endpoint at a time.
- Decommission infrastructure — once fully migrated, tear down the EC2 instances.
Drop-in Replacement
If your current Puppeteer call looks like this:
// Before: Puppeteer
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const buffer = await page.screenshot({ type: 'png', fullPage: true });
await page.close();
The PageBolt equivalent:
// After: PageBolt 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, fullPage: true })
});
const buffer = Buffer.from(await response.arrayBuffer());
Same result. No browser to manage. No memory leaks. No 2 AM pager alerts.