We have all been there. You write a script that works perfectly on localhost. It grabs the data, parses the HTML, and logs the result. But the moment you scale it up—trying to process ten thousand pages instead of ten—the cracks appear. Memory leaks, connection timeouts, IP bans, and an execution time that stretches from minutes into hours.
Most developers treat scraping as a "grab and go" task, but at scale, it is an engineering discipline. It requires moving from simple HTTP requests to a robust pipeline.
If you are tired of Puppeteer eating all your RAM for breakfast or struggle to make your requests concurrency-safe, it is time to return to the basics: Cheerio and Axios. This combination isn't just "lighter"—when architected correctly, it is orders of magnitude faster.
Here is how to turn a simple script into a high-performance data extraction engine.
Why Is the "Heavy" Browser Stack Often the Wrong Choice?
When starting a scraping project, the default advice often points to headless browsers like Puppeteer or Playwright. While indispensable for Single Page Applications (SPAs) heavily reliant on client-side JavaScript, they are overkill for static content.
Running a full browser instance (even headless) involves rendering the DOM, executing CSS, and running JavaScript. This consumes significant CPU and memory.
The Cost of Rendering
For every page you load in Puppeteer, you are paying a "rendering tax." If your target data exists in the initial HTML document response, paying this tax is engineering waste.
By stripping away the browser engine and dealing with raw HTTP responses (Axios) and parsing the static HTML text (Cheerio), you reduce the overhead per request from hundreds of milliseconds (or seconds) to mere milliseconds.
The Core Framework: The Fetch-Parse-Pipeline
To build a high-speed scraper, we shouldn't just think in terms of "functions." We should think in terms of a pipeline. This framework makes your code modular, testable, and capable of handling errors gracefully.
- The Fetcher (Axios): Responsible strictly for network transport. It handles retries, headers, and proxy rotation.
- The Parser (Cheerio): Responsible for traversing the HTML text. It should be pure logic—input HTML, output JSON.
- The Orchestrator: Manages concurrency, rate limiting, and data persistence. Let’s break down the optimal configuration for each.
Phase 1: Configuring Axios for Resilience
A standard axios.get() is insufficient for production scraping. You need to mimic a real browser's networking behavior to avoid instant detection and manage unstable connections.
1. Keep-Alive Agents
By default, Node.js creates a new TCP connection for every request. This is slow. We need to reuse connections.
const axios = require('axios');
const http = require('http');
const https = require('https');
// Create persistent agents
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });
const client = axios.create({
httpAgent,
httpsAgent,
timeout: 10000, // Hard timeout to prevent hanging sockets
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', // Key for avoiding 403s
'Accept-Encoding': 'gzip, deflate, br' // crucial for bandwidth reduction
}
});
2. Pattern: The "Exponential Backoff" Retry
Network errors are inevitable. A simple loop is dangerous because it can Hammer a server that is already struggling. Use interceptors to implement "Exponential Backoff"—waiting longer after each failed attempt.
client.interceptors.response.use(null, async (error) => {
const config = error.config;
// Initialize retry count
if (!config || !config.retry) return Promise.reject(error);
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount >= config.retry) {
return Promise.reject(error);
}
config.__retryCount += 1;
// Calculate delay: 2^retryCount * 1000ms
const backoff = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, (Math.pow(2, config.__retryCount) * 1000));
});
await backoff;
return client(config);
});
Using this setup ensures that temporary 502/503 errors don't crash your scraper, but rather pause it intelligently.
Phase 2: High-Speed Parsing with Cheerio
Cheerio implements a subset of jQuery designed specifically for the server. It does not produce a visual rendering; it constructs a Document Object Model (DOM) from a string.
Optimizing Selection
The speed of Cheerio depends heavily on how specific your selectors are. Generic selectors force the library to traverse the entire tree.
-
Avoid:
$('.class')(Searches everywhere) -
Prefer:
$('div.main-container > ul > li.item')(Scoped traversal)
const cheerio = require('cheerio');
const parseProduct = (html) => {
// Load HTML only once
const $ = cheerio.load(html);
// Use map for lists to keep memory allocation efficient
const products = $('.product-list .item').map((_, el) => {
const $el = $(el);
return {
title: $el.find('h2.title').text().trim(),
price: $el.find('.price').attr('data-value'), // Prefer attributes meant for data
inStock: !$el.hasClass('out-of-stock')
};
}).get(); // .get() converts cheerio object to genuine array
return products;
};
Memory Management in Parsing
One common mistake is loading Cheerio inside a loop and never dereferencing it. If you are processing thousands of pages, ensure the html string and the $ instance go out of scope so Garbage Collection (GC) can reclaim the memory. Encapsulate parsing logic in pure functions that return data structures, not Cheerio objects.
How Do We Scale Concurrency Without Crashing?
This is where the distinction between "working" and "engineering" lies. If you fire 10,000 requests in a Promise.all(), you will hit system file descriptor limits (EMFILE errors) or get IP-banned instantly.
We need a Queue.
While libraries like p-queue or bottleneck are excellent, understanding the pattern is more valuable. We want to maintain a constant number of "workers" running in parallel.
The "Worker Pool" Pattern
Instead of pushing requests, imagine having 5 workers (concurrent slots). As soon as one worker finishes a job, it pulls the next URL from the stack.
async function worker(id, queue, results) {
while (queue.length > 0) {
const url = queue.shift();
try {
console.time(`Worker ${id}: ${url}`);
const { data } = await client.get(url, { retry: 3 });
const parsedData = parseProduct(data);
results.push(parsedData);
console.timeEnd(`Worker ${id}: ${url}`);
} catch (err) {
console.error(`Failed ${url}: ${err.message}`);
// Strategy: Push back to queue or log to "dead letter" file
}
}
}
async function orchestrator(urls, concurrency = 5) {
const queue = [...urls]; // Clone to safely mutate
const results = [];
// Create an array of worker promises
const workers = Array.from({ length: concurrency }).map((_, i) =>
worker(i, queue, results)
);
await Promise.all(workers);
return results;
}
This manual implementation gives you total control. You can dynamically adjust concurrency based on server response times or stop the queue entirely if you detect a "soft ban" (like a Captcha challenge).
Dealing with Anti-Bot Defenses
High speed can trigger defensive mechanisms. While Cheerio/Axios cannot solve Captchas (you need browsers for that), avoiding detection is largely about Fingerprinting and Behavior.
-
Header consistency: Ensure your headers match the Request Format of the
User-Agentyou are spoofing. Do not send "Postman-Token" or default Axios headers. - TLS Fingerprinting: Sophisticated sites analyze the TLS/SSL handshake. Node’s default TLS handshake is easily identifiable.
-
Insight: For high-security targets, consider using wrappers like
ciphersin the HTTPS agent options to randomize the cipher suites sent during the handshake, mimicking Chrome or Firefox. - Proxy Rotation: The single most effective tool. If scaling high, single IP scraping is dead. Integrate a proxy service at the Axios config level.
Step-by-Step Guide: The "High Velocity" Checklist
If you are building a new scraper today, follow this progression to ensure stability and speed.
- Analyze the Target (Network Tab): Before writing code, inspect the XHR/Fetch requests in Chrome DevTools. Can you hit a JSON API instead of parsing HTML? (This is always faster than Cheerio).
- Setup Axios Instance:
- Enable
keepAlive. - Set a realistic
User-Agent. - Configure timeouts (never utilize default unlimited timeouts).
- Implement the Retry Strategy:
- Add an interceptor for 5xx errors.
- Implement exponential backoff logic.
- Develop the Parser (Cheerio):
- Verify selectors with
trim()to clean generic whitespace. - Extract into a pure function for isolated testing.
- Orchestrate Concurrency:
- Implement a queue system (start with concurrency = 5).
- Monitor memory usage. If heap grows indefinitely, check for variable leaks in your loop.
- Data Persistence:
- Do not keep 10k items in memory. Stream results to a JSONL (JSON Lines) file or database immediately after parsing.
Final Thoughts
Technology stacks are rarely about "good" or "bad"—they are about fit. While the allure of headless AI-driven browsers is strong, there is a distinct elegance in the raw efficiency of HTTP scraping.
When you strip away the rendering engine, you are essentially engaging in a direct dialogue with the server. By combining the low-level control of Axios with the parsing speed of Cheerio, you are building a tool that respects system resources.
The next time you face a scraping challenge, don't ask "How do I automate the browser?" Ask "How do I get the data?" The answer might just be a few kilobytes of HTML and a well-tuned Node.js script.
Keep your headers clean, your agents persistent, and your parsers pure.
Top comments (0)