Here's the complete tab-pooling.md:
# π Tab Pooling β Concurrent Requests
Handle multiple simultaneous requests to the same site without collisions. Perfect for API servers handling concurrent traffic.
---
## Overview
By default, each registered site gets **ONE tab**. If multiple API requests hit the same site simultaneously, they collide β one navigates while another is mid-scrape, causing race conditions and failed requests.
Tab pooling gives each site a pool of tabs. Concurrent requests each get their own tab. Requests beyond pool size queue and wait for a free tab.Without Pooling (Default): With Pooling (pool: 3): βββββββββββββββββββββ βββββββββββββββββββββ Request 1 βββΊ Tab 1 Request 1 βββΊ Tab 1 Request 2 βββΊ Tab 1 (collision!) Request 2 βββΊ Tab 2 Request 3 βββΊ Tab 1 (collision!) Request 3 βββΊ Tab 3 Request 4 βββΊ Queue (waits)
---
## Basic Usage
```ts
import piggy, { usePiggy } from "nothing-browser";
await piggy.launch({ mode: "tab" });
// Single tab (default) β requests serialize
await piggy.register("amazon", "https://www.amazon.com");
// Pool of 3 tabs β 3 concurrent requests supported
await piggy.register("amazon", "https://www.amazon.com", { pool: 3 });
// Pool of 5 tabs β for heavy traffic
await piggy.register("amazon", "https://www.amazon.com", { pool: 5 });Check Pool Status β
Monitor your pool at runtime:
const { amazon } = usePiggy<"amazon">();
// Get current pool statistics
const stats = amazon.poolStats();
console.log(stats);
// { idle: 2, busy: 1, queued: 0, total: 3 }Pool Stats Object β
| Field | Description |
|---|---|
idle | Free tabs ready for use |
busy | Tabs currently handling requests |
queued | Requests waiting for a free tab |
total | Total pool size (idle + busy) |
Real-time Monitoring β
// Log pool status every 5 seconds
setInterval(() => {
const stats = amazon.poolStats();
console.log(`[Pool] ${stats.idle}/${stats.total} idle, ${stats.queued} queued`);
}, 5000);
// Alert if queue gets too long
setInterval(() => {
const stats = amazon.poolStats();
if (stats.queued > 10) {
console.warn(`β οΈ Pool queue length: ${stats.queued}`);
}
}, 10000);When to Use Pooling β
| Scenario | Recommended Pool Size | Why |
|---|---|---|
| Single user, sequential scraping | 1 (default) | No concurrency needed |
| Local development | 1 | Keep it simple |
| API server, light traffic (<10 req/sec) | 2β3 | Handle occasional concurrency |
| API server, medium traffic (10-50 req/sec) | 4β6 | Balance performance and memory |
| API server, heavy traffic (>50 req/sec) | 6β10 | High concurrency |
| Production load | 8+ | Maximum throughput |
Performance Impact β
| Pool Size | Memory Usage | Max Concurrent | Best For |
|---|---|---|---|
| 1 | Low | 1 | Sequential tasks |
| 2-3 | Medium | 2-3 | Light API traffic |
| 4-6 | High | 4-6 | Production APIs |
| 8+ | Very High | 8+ | Heavy concurrent load |
Warning: Each tab opens a real browser tab and consumes memory. Don't set pool sizes unnecessarily high. Start small and monitor.
How It Works β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Request β
β GET /amazon/search?q=laptop β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tab Pool (size: 3) β
β β
β ββββββββ ββββββββ ββββββββ ββββββββββββββββ β
β β Tab1 β β Tab2 β β Tab3 β β Queue (FIFO) β β
β β BUSY β β IDLE β β IDLE β β Request 4 β β
β ββββββββ ββββββββ ββββββββ β Request 5 β β
β ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Request Execution β
β β
β 1. Request arrives β
β 2. Grab first idle tab (or queue if none) β
β 3. Execute navigation/scraping β
β 4. Release tab back to pool β
β 5. Next queued request takes the tab β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββRequest Flow β
- Request arrives at your API endpoint
- Pool checks for idle tab - if available, grabs it immediately
- No idle tab available - request goes to FIFO queue
- Tab completes work - releases back to pool
- Queue processed - next waiting request gets the freed tab
Complete API Server Example β
import piggy, { usePiggy } from "nothing-browser";
await piggy.launch({ mode: "tab", binary: "headless" });
// Register with pool of 4 tabs for concurrent requests
await piggy.register("amazon", "https://www.amazon.com", { pool: 4 });
const { amazon } = usePiggy<"amazon">();
// Enable human mode for all tabs
piggy.actHuman(true);
// API endpoint that uses the pool
await amazon.api("/search", async (_params, query) => {
const term = query.q ?? "laptop";
const pages = parseInt(query.pages) ?? 3;
// This runs in its own tab from the pool
// Multiple concurrent requests each get their own tab
const results = [];
for (let page = 1; page <= pages; page++) {
await amazon.navigate(`https://www.amazon.com/s?k=${encodeURIComponent(term)}&page=${page}`);
await amazon.wait(2000);
const pageResults = await amazon.evaluate(() =>
Array.from(document.querySelectorAll("[data-asin]")).map(el => ({
asin: el.getAttribute("data-asin"),
title: el.querySelector("h2 span")?.textContent,
price: el.querySelector(".a-price-whole")?.textContent,
}))
);
results.push(...pageResults);
await amazon.wait(1000);
}
// Log pool status after request
console.log("Pool after request:", amazon.poolStats());
return {
term,
pages,
count: results.length,
results,
pool: amazon.poolStats() // Include in response for monitoring
};
}, {
ttl: 30000, // Cache for 30 seconds
detail: {
summary: "Search Amazon products",
parameters: [
{ name: "q", in: "query", schema: { type: "string", default: "laptop" } },
{ name: "pages", in: "query", schema: { type: "integer", default: 3 } }
]
}
});
// Health check endpoint with pool status
await amazon.api("/health", async () => {
const stats = amazon.poolStats();
return {
status: "ok",
timestamp: Date.now(),
pool: stats,
healthy: stats.queued < 10 // Alert if queue backing up
};
});
// Start server
await piggy.serve(3000, {
title: "Amazon Scraper API",
version: "1.0.0",
description: "Product search with concurrent tab pooling"
});
console.log("π API running on http://localhost:3000");
console.log("π Pool size: 4 tabs");
// Keep site alive
amazon.noclose();Testing Concurrent Requests β
Test your pool with simultaneous requests:
// Test script - run this separately
async function testConcurrency() {
const requests = [];
// Fire 10 concurrent requests
for (let i = 0; i < 10; i++) {
requests.push(
fetch(`http://localhost:3000/amazon/search?q=laptop&pages=1`)
.then(res => res.json())
.then(data => ({ request: i + 1, count: data.count, pool: data.pool }))
);
}
const results = await Promise.all(requests);
console.table(results);
}
testConcurrency();Expected output with pool size 4:
- First 4 requests execute immediately
- Requests 5-10 queue and wait
- All complete successfully without collisions
Pool Management β
Dynamic Pool Resizing (Coming Soon) β
// Future feature - resize pool at runtime
await amazon.resizePool(6); // Increase to 6 tabs
await amazon.resizePool(2); // Decrease to 2 tabs (busy tabs complete first)Graceful Tab Shutdown β
// Wait for all busy tabs to complete before closing
await piggy.close(); // Waits for all pool tabs to finish
// Force close (kills all tabs immediately)
await piggy.close({ force: true });Per-Request Tab Timeout β
// Set timeout for each request (prevents hung tabs)
await amazon.api("/slow-search", async () => {
// This will timeout after 30 seconds
// Tab is released back to pool
await amazon.wait(60000); // Would timeout
}, { timeout: 30000 });Pool vs No Pool Comparison β
Without Pooling (Default) β
await piggy.register("amazon", "https://amazon.com");
// 3 concurrent requests to /search
// Request 1: Uses tab
// Request 2: Waits for Request 1 to finish
// Request 3: Waits for Request 2 to finish
// Total time: ~9 seconds (3 * 3 seconds each)With Pooling (pool: 3) β
await piggy.register("amazon", "https://amazon.com", { pool: 3 });
// 3 concurrent requests to /search
// Request 1: Uses tab 1
// Request 2: Uses tab 2
// Request 3: Uses tab 3
// Total time: ~3 seconds (all run in parallel)Speed improvement: 3x faster with pool size 3
Best Practices β
1. Start Small, Monitor, Adjust β
// Start with conservative pool size
await piggy.register("api", "https://example.com", { pool: 2 });
// Monitor memory usage
setInterval(() => {
const memUsage = process.memoryUsage();
console.log(`Memory: ${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`);
}, 30000);
// Adjust based on metrics2. Match Pool to API Traffic β
// Low traffic API - small pool
await piggy.register("internal", "https://internal.com", { pool: 1 });
// External API with high traffic - larger pool
await piggy.register("amazon", "https://amazon.com", { pool: 6 });3. Cache Aggressively to Reduce Pool Pressure β
await amazon.api("/search", handler, {
ttl: 60000, // Cache for 1 minute - fewer pool requests
pool: 4
});4. Monitor Queue Length β
setInterval(() => {
const stats = amazon.poolStats();
if (stats.queued > stats.total * 2) {
console.error(`β οΈ Queue backlog: ${stats.queued} waiting`);
// Alert your team or scale up
}
}, 10000);Troubleshooting β
"Pool queue growing indefinitely" β
Symptoms: queued keeps increasing, never decreases
Solutions:
- Increase pool size:
{ pool: 8 } - Check if requests are hanging (add timeouts)
- Check if you're releasing tabs properly
High memory usage β
Symptoms: Node.js memory keeps growing
Solutions:
- Decrease pool size
- Add
ttlto cache responses - Close idle tabs:
await amazon.closeIdleTabs()
"Tab crashed" errors β
Symptoms: Random failures, tab stops responding
Solutions:
- Pool automatically recreates crashed tabs
- Check if site crashes headless browsers
- Try
binary: "headful"to debug
API Reference β
| Method | Description |
|---|---|
piggy.register(name, url, { pool: N }) | Register site with N-tab pool |
site.poolStats() | Get pool statistics: { idle, busy, queued, total } |
Register Options β
interface RegisterOptions {
pool?: number; // Number of tabs in pool (default: 1)
binary?: "headless" | "headful"; // Override binary for this site
}Next Steps β
- Built-in API Server β Combine with API routes
- Multi-Site Parallel β Run multiple sites simultaneously
- Data Storage β Save scraped data with validation
Nothing Ecosystem Β· Ernest Tech House Β· Kenya Β· 2026