PRODUCTION RECIPE · 2D FAN-OUT & RESUMABLE PIPELINES

High-Throughput Parallel Fleet Scraping

How AI agents scale data harvesting using two-dimensional parallelism: spreading workloads across N Chrome profiles and M concurrent tabs with fault-tolerant state persistence.

Quick Answer (TL;DR) WebBridge provides two dimensions of concurrency: cross-profile (independent browser instances with their own cookies and storage) × per-tab (each tab attached to its own CDP debugger session). For a worklist of 1,000 URLs, split items across 4 profiles × 4 tabs each (16 concurrent browser operations) and stream output to a resumable .jsonl ledger.

1. The Two-Dimensional Fan-Out Architecture

Standard browser automations lock execution to a single active tab or single profile. In WebBridge, every tab maintains an independent CDP connection through the MV3 extension:

Fleet Concurrency = (N Profiles) × (M Tabs per Profile)

Example Configuration:
  • Profile 1 ("Work"):     Tab 1 (s1) ║ Tab 2 (s1) ║ Tab 3 (s1) ║ Tab 4 (s1)
  • Profile 2 ("Personal"): Tab 1 (s2) ║ Tab 2 (s2) ║ Tab 3 (s2) ║ Tab 4 (s2)
  • Profile 3 ("Research"): Tab 1 (s3) ║ Tab 2 (s3) ║ Tab 3 (s3) ║ Tab 4 (s3)
  Total Concurrency: 12 live pages evaluating in parallel

2. The Resumable .jsonl Ledger Pattern

When running large scraping jobs, never buffer results in memory. Append output incrementally to a JSON Lines (.jsonl) file after every page evaluation. If the process is paused or aborted, resume directly from the remaining items:

import fs from 'node:fs';
import { WebBridgeClient } from 'agent-webbridge';

// Read previously processed URLs from disk
function loadDoneUrls(logFile) {
  if (!fs.existsSync(logFile)) return new Set();
  const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n');
  return new Set(lines.map(line => {
    try { return JSON.parse(line).url; } catch { return null; }
  }).filter(Boolean));
}

// Write result incrementally
function appendResult(logFile, data) {
  fs.appendFileSync(logFile, JSON.stringify(data) + '\n');
}

3. Production Implementation (Node.js)

import { WebBridgeClient } from "agent-webbridge";

const TARGET_URLS = [
  "https://news.ycombinator.com/item?id=39000001",
  "https://news.ycombinator.com/item?id=39000002",
  "https://news.ycombinator.com/item?id=39000003",
  "https://news.ycombinator.com/item?id=39000004",
];

async function harvestUrl(client, url) {
  // Open tab specifically inside this session
  const nav = await client.navigate(url, { newTab: true });
  
  // Extract data using in-page evaluate
  const result = await client.evaluate(`(() => {
    const title = document.querySelector('.titleline a')?.innerText || '';
    const score = document.querySelector('.score')?.innerText || '0';
    return { title, score };
  })()`);

  // Close tab immediately after extraction to keep memory clean
  await client.closeTab();

  return { url, ...result };
}

async function runFleet() {
  const client = new WebBridgeClient({ profile: "Work", session: "harvest-job" });

  // Concurrently process in batches of 4
  const CONCURRENCY = 4;
  for (let i = 0; i < TARGET_URLS.length; i += CONCURRENCY) {
    const chunk = TARGET_URLS.slice(i, i + CONCURRENCY);
    const results = await Promise.all(chunk.map(url => harvestUrl(client, url)));
    console.log(`Processed batch of ${results.length} items`);
  }

  // Cleanup tab group
  await client.closeSession();
}

runFleet().catch(console.error);

4. Defensive Egress IP Pacing

Because all local Chrome profiles share the machine's egress IP address: