Intermediate Guide
Batch Processing Guide
Scale web scraping to thousands of URLs with efficient queue management, error recovery, and performance optimization strategies.
1. Using batch_scrape Tool
The batch_scrape tool fetches up to 50 URLs concurrently in a single synchronous request. Every URL comes back with its own status, so one dead page never sinks the batch.
Basic Batch Scraping
5 credits per URL attempted (50 URLs = 250 credits)
Bash
curl -X POST https://crawlforge.dev/api/v1/tools/batch_scrape \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
{ "url": "https://example.com/page1" },
{ "url": "https://example.com/page2" },
{ "url": "https://example.com/page3" }
],
"batch_config": { "concurrency": 5 }
}'No async mode on the REST API:
batch_scrape over HTTP is synchronous - one request in, finished results out. There is no job id to poll and no webhook: batch_id is a retrieval key for get_batch_results, not a job handle. Chunk anything larger than 50 URLs as shown below. The CrawlForge MCP server can run the same 50-URL batch in the background with mode: 'async' and an optional webhook.2. Queue Management
Process thousands of URLs by chunking them into batches and managing a queue.
Chunking Strategy
Break large URL lists into manageable batches
Typescript
// Chunk array into batches of 50
function chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// Process all URLs in batches
async function processBatches(urls: string[]) {
const batches = chunkArray(urls, 50); // Max 50 URLs per batch
const allResults: any[] = [];
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
console.log(`Processing batch ${i + 1}/${batches.length}...`);
const response = await fetch('https://crawlforge.dev/api/v1/tools/batch_scrape', {
method: 'POST',
headers: {
'X-API-Key': process.env.CRAWLFORGE_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
urls: batch.map(url => ({ url })),
batch_config: { concurrency: 8 },
}),
});
const data = await response.json();
allResults.push(...data.data.results);
// Wait between batches to respect rate limits
if (i < batches.length - 1) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
return allResults;
}
// Usage
const urls = [...]; // 500 URLs
const results = await processBatches(urls);
console.log(`Scraped ${results.length} total pages`);Pro Tip: Use Redis or a database to store your queue. This allows you to resume processing if your script crashes or needs to restart.
3. Error Recovery
Handle failures gracefully with retry logic and error tracking.
Robust Error Handling
Typescript
interface BatchResult {
successful: any[];
failed: { url: string; error: string }[];
}
async function batchScrapeWithRetry(
urls: string[],
maxRetries = 3
): Promise<BatchResult> {
const successful: any[] = [];
const failed: { url: string; error: string }[] = [];
let remainingUrls = [...urls];
let retries = 0;
while (remainingUrls.length > 0 && retries <= maxRetries) {
try {
const response = await fetch('https://crawlforge.dev/api/v1/tools/batch_scrape', {
method: 'POST',
headers: {
'X-API-Key': process.env.CRAWLFORGE_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
urls: remainingUrls.map(url => ({ url })),
batch_config: { concurrency: 5 },
}),
});
const data = await response.json();
// Each result carries its own status: 'success' | 'failed' | 'skipped'.
// 'skipped' means the request's time budget ran out before that URL was
// started — those are not charged, so they are always worth retrying.
const batchSuccessful = data.data.results.filter((r: any) => r.status === 'success');
const batchUnfinished = data.data.results.filter((r: any) => r.status !== 'success');
successful.push(...batchSuccessful);
// Only retry the URLs that did not come back successful
remainingUrls = batchUnfinished.map((r: any) => r.url);
if (remainingUrls.length > 0) {
console.log(`Retrying ${remainingUrls.length} failed URLs...`);
retries++;
await new Promise(resolve => setTimeout(resolve, 2000 * retries));
}
} catch (error) {
console.error('Batch request failed:', error);
retries++;
if (retries > maxRetries) {
// Mark all remaining URLs as failed
failed.push(...remainingUrls.map(url => ({
url,
error: String(error)
})));
break;
}
await new Promise(resolve => setTimeout(resolve, 2000 * retries));
}
}
return { successful, failed };
}
// Usage
const { successful, failed } = await batchScrapeWithRetry(urls);
console.log(`Success: ${successful.length}, Failed: ${failed.length}`);
// Save failed URLs for manual review
if (failed.length > 0) {
fs.writeFileSync('failed-urls.json', JSON.stringify(failed, null, 2));
}4. Performance Optimization
Maximize throughput and minimize costs with these optimization strategies.
Optimize Concurrency
Start with
batch_config.concurrency: 5. The API accepts 1-10 and runs at most 8 workersMind the Time Budget
One request fetches for about 20s. URLs it never starts come back
skipped - and skipped URLs are not chargedRe-read, Don't Re-scrape
Results are stored for 24h. Page through them with
get_batch_results (1 credit) instead of repeating the batch (5 credits per URL)Cache Results
Store scraped data in Redis/database to avoid re-scraping same URLs
Avoid Over-Batching
Don't exceed 50 URLs per batch - split into multiple requests instead
Don't Ignore Rate Limits
Respect your plan's rate limits (Free: 1/s, Hobby: 2/s, Pro: 4/s, Business: 10/s)
Expected Performance
| Scenario | Time | Settings |
|---|---|---|
| Small Batch (10 URLs) | ~5 seconds | concurrency: 5 |
| Medium Batch (50 URLs) | ~15 seconds | concurrency: 8 |
| Large Batch (500 URLs) | ~3 minutes | 10 batches × 50 URLs |
| Massive Batch (5,000 URLs) | ~30 minutes | 100 batches × 50 URLs |
Next Steps
Continue learning with more advanced guides