Node 18+ ships with fetch built in, so the question is no longer whether to use Axios or node-fetch. For a single GET, built-in fetch is enough. For a scraper making thousands of requests with proxy rotation, retry logic, and interceptors, Axios is less code.
A Quick Overview of Fetch and Axios
To compare the two effectively, it helps to understand what each one is and what it was designed for.
What is Fetch API
The Fetch API is a native JavaScript interface for making HTTP requests. It is part of the Window and Worker APIs, providing a global fetch() method to send requests. This API was designed to replace the legacy XMLHttpRequest approach, offering a simpler tool for working with network requests.
A simple GET request using fetch():
fetch('https://example.com')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('There was a problem with your fetch operation:', error));fetch has been part of Node.js since version 18. The node-fetch package was the polyfill for Node < 18 and is no longer needed in 2026.
What is Axios
Axios is a JavaScript library for making HTTP requests from both browsers and Node.js. It provides a convenient API for making requests to servers and handling responses. Compared to fetch(), Axios adds several features including automatic JSON response parsing and XSRF protection.
It is often used alongside the Cheerio library for web scraping. Cheerio is a fast implementation of core jQuery designed for the server, letting you select and extract data from HTML documents.
To use Axios in the browser, add a script tag from the CDN to your HTML file:
axios.get('https://example.com')
.then(function (response) {
console.log(response.data);
})
.catch(function (error) {
console.log(error);
})
.finally(function () {
});To use Axios in Node.js, install the package using npm or yarn. Other than that, there are not many differences:
const axios = require('axios');
axios.get('https://example.com')
.then(function (response) {
console.log(response.data);
})
.catch(function (error) {
console.log(error);
})
.finally(function () {
});Key Differences and Considerations
The table below covers the main feature differences between the two clients.
| Parameter | Fetch | Axios |
|---|---|---|
| Installation | Built-in in most modern browsers | Requires installation through NPM/Yarn |
| Browser Compatibility | Supported by most modern browsers, but requires polyfills for IE | Supported by all modern browsers and IE |
| JSON Handling | Requires calling .json() on the response | Automatically transforms JSON to/from objects |
| Error Handling | Does not reject promises for HTTP error responses | Automatically rejects promises for HTTP error responses |
| Timeouts | No built-in support | Supports timeouts out of the box |
| Download Progress | Not natively supported | Supports download progress tracking |
| CORS (Cross-Origin Resource Sharing) | Supported | Supported |
| CSRF/XSRF Protection | No built-in support | Built-in support |
| Interceptors | Not supported | Supported |
| Default Configuration | Limited support | Easily configurable global settings |
| Node.js Usage | Built in since Node 18 | Supported out of the box |
| Library Size | Smaller (built into browsers by default) | Larger (due to additional features) |
| Promises & async/await | Easily used with promises and async/await | Requires chaining with .then() or using async/await |
| Built-in Methods | Directly uses browser’s built-in methods (Headers, Request, Response) | Requires axios.get(), axios.post(), etc. |
| Global Settings | Utilizes browser’s global settings (e.g., headers, caching) | Easily configurable global settings |
| Convenience in Browser | Native browser API, simplifying debugging in DevTools | May require source maps for debugging in DevTools |
| Technology | ES6+ standard | Library, not part of the JavaScript standard |
Syntax and Response object
The Fetch API is a native JavaScript feature, eliminating the need for external libraries. While GET requests are covered above, a POST request better illustrates the differences. Here’s how a POST request looks with fetch():
fetch('https://example.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
key1: 'value1',
key2: 'value2',
}),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('There was a problem with your fetch operation:', error));Axios is an external library that provides a more concise syntax. The same request:
axios.post('https://example.com', {
key1: 'value1',
key2: 'value2',
})
.then(response => console.log(response.data))
.catch(error => console.error('There was a problem with your axios operation:', error));Axios reduces the amount of code and improves readability, which is why many developers prefer it.
Performance considerations
Performance between the two clients is closer than most comparisons suggest. I ran 1,000 requests against a local HTTP server at four concurrency levels on Node 22 to measure throughput and peak RSS:
| Client | Concurrency | Time (ms) | req/s | Peak RSS (MB) |
|---|---|---|---|---|
fetch | 1 | 1,258 | 795 | 88 |
axios | 1 | 1,092 | 916 | 99 |
fetch | 10 | 843 | 1,186 | 102 |
axios | 10 | 780 | 1,282 | 109 |
fetch | 50 | 700 | 1,428 | 118 |
axios | 50 | 744 | 1,344 | 121 |
fetch | 200 | 864 | 1,158 | 128 |
axios | 200 | 954 | 1,049 | 139 |
At low concurrency (c=1, c=10), axios is marginally faster. At c=50 and c=200, fetch edges ahead. The gap stays under 15% at every level, and both clients report zero failures on 1,000 requests. Fetch uses roughly 10 MB less RSS across all concurrency levels. On a real scraping target with network latency, SSL overhead, and variable server response times, these differences shrink further.

Response Timeout
The Fetch API does not offer a direct way to set a timeout. The approach is AbortController with AbortSignal to cancel a request after a set amount of time:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch('https://example.com/', { signal: controller.signal })
.then(function (response) {
clearTimeout(timeoutId);
return response.json();
})
.then(function (data) {
console.log(data);
})
.catch(function (error) {
if (error.name === 'AbortError') {
console.log('Request canceled due to timeout');
} else {
console.log(error);
}
});Axios allows setting a timeout with the timeout option in the request configuration. If the timeout is exceeded, the request is aborted and the promise rejects:
axios.get('https://example.com', { timeout: 5000 })
.then(function (response) {
console.log(response.data);
})
.catch(function (error) {
if (axios.isCancel(error)) {
console.log('Request canceled due to timeout');
} else {
console.log(error);
}
});Both solutions offer similar functionality, but Axios provides a more direct way to manage request timeouts.
Error Handling
Fetch API considers any HTTP status codes other than 200 (success) as an error. However, this is not automatic, and you need to check the status of your code to handle errors manually. Additionally, fetch() does not handle network errors (such as no internet connection or inability to establish a connection). To handle such errors, use try-catch around the fetch() call:
try {
const response = await fetch('https://example.com');
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch error:', error);
}Axios automatically handles HTTP statuses and triggers the catch block for all responses with error HTTP statuses (except 2xx). This simplifies error handling. Additionally, Axios handles network errors such as no connection or inability to establish a connection:
axios.get('https://example.com')
.then(response => console.log(response.data))
.catch(error => {
if (error.response) {
console.error('Axios error! HTTP status:', error.response.status);
} else if (error.request) {
console.error('Axios error! Network issue:', error.message);
} else {
console.error('Axios error!', error.message);
}
});Axios offers a more convenient error-handling mechanism, especially for HTTP statuses. The fetch() requires more manual handling, particularly for network errors.
Upload Progress
Monitoring file upload progress with Axios is more straightforward than with fetch(). Axios provides the onUploadProgress parameter, which triggers whenever the upload progress updates:
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');
const fileStream = fs.createReadStream('file.txt');
const formData = new FormData();
formData.append('file', fileStream);
axios.post('https://example.com/', formData, {
headers: formData.getHeaders(),
onUploadProgress: progressEvent => {
console.log(`Uploaded ${progressEvent.loaded} bytes`);
}
})
.then(response => {
console.log('Upload successful');
})
.catch(error => {
console.error('Upload failed:', error);
});Tracking upload progress with the Fetch API requires deeper integration with the response data stream:
const file = document.querySelector('input[type="file"]').files[0];
const formData = new FormData();
formData.append('file', file);
fetch('https://example.com/', {
method: 'POST',
body: formData
}).then(response => {
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length');
let loaded = 0;
reader.read().then(function processResult(result) {
if (result.done) {
console.log('Upload completed');
return;
}
loaded += result.value.length;
console.log(`Uploaded ${loaded} of ${contentLength} bytes`);
return reader.read().then(processResult);
});
}).catch(error => {
console.error('Upload failed:', error);
});If your application heavily relies on file uploading, Axios can significantly simplify this functionality.
Backward-Compatibility
Fetch API is supported by most modern browsers but not by Internet Explorer and older versions of other browsers. Polyfills are required to ensure backward compatibility with outdated browsers.
Axios provides better backward compatibility because it can be used in browsers and Node.js without additional polyfills. This makes it a preferable choice for projects that require support for legacy browsers or environments.
Automatic JSON Data Transformation
The Fetch API does not automatically parse JSON responses. To get the data in JSON format, you need to call the .json() method on the response object:
fetch('https://example.com/')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));Axios offers automatic JSON data parsing. The response data is automatically extracted and accessible through the .data property:
axios.get('https://example.com/')
.then(response => console.log(response.data))
.catch(error => console.error('Error:', error));If the server returns data in a format that cannot be converted to JSON, Axios will generate an error. You can use methods such as .text() or .arrayBuffer() to get the data in the desired format when needed.
HTTP Interceptors
HTTP interceptors allow you to intercept and process requests and responses before they are sent or after they are received. They are useful for adding common logic to all HTTP requests, such as authentication, authorization, or logging. Error handling and data manipulation are also common uses.
The Fetch API does not provide a built-in mechanism to intercept HTTP requests. You need to explicitly handle Request and Response objects in your code:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));Axios provides a built-in interceptor mechanism for both requests and responses:
const axios = require('axios');
axios.interceptors.request.use(config => {
console.log('Request Interceptor:', config);
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
console.log('Response Interceptor:', response);
return response;
}, error => {
return Promise.reject(error);
});
axios.get('https://example.com/')
.then(response => console.log(response.data))
.catch(error => console.error('Error:', error));Axios simplifies adding common logic to all requests without needing to explicitly handle requests and responses at every call site.
Download Progress
Fetch API provides a Response object with a body.getReader() method that returns a ReadableStream. Reading data in chunks allows you to track download progress:
fetch('https://example.com/')
.then(response => {
const contentLength = response.headers.get('Content-Length');
const total = parseInt(contentLength, 10);
let loaded = 0;
const reader = response.body.getReader();
function read() {
return reader.read().then(({ done, value }) => {
if (done) {
console.log('Download complete');
return;
}
loaded += value.byteLength;
console.log(`Progress: ${(loaded / total) * 100}%`);
return read();
});
}
return read();
})
.catch(error => console.error('Download error:', error));Axios provides an onDownloadProgress callback function that is called periodically with information about the bytes downloaded:
const axios = require('axios');
const url = 'https://example.com/';
axios({
method: 'get',
url: url,
responseType: 'stream',
onDownloadProgress: progressEvent => {
const total = progressEvent.lengthComputable ? progressEvent.total : -1;
const loaded = progressEvent.loaded;
if (total !== -1) {
console.log(`Progress: ${(loaded / total) * 100}%`);
}
},
})
.then(response => {
console.log('Download complete');
})
.catch(error => console.error('Download error:', error));For precise progress tracking, body.getReader() gives more control. For a general progress indicator, onDownloadProgress is the simpler option.
Simultaneous Requests
Both Fetch API and Axios support concurrent requests. The native approach with fetch uses Promise.all:
const urls = ['https://example.com/data1', 'https://example.com/data2'];
const requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(responses => Promise.all(responses.map(response => response.json())))
.then(data => console.log(data))
.catch(error => console.error(error));Axios uses the same Promise.all pattern:
const axios = require('axios');
const urls = ['https://api.example.com/data1', 'https://api.example.com/data2'];
const [res1, res2] = await Promise.all(urls.map(url => axios.get(url)));
console.log(res1.data, res2.data);For concurrent requests at scale, Promise.all with a concurrency limiter (such as p-limit) applies to both clients equally.
Handling CORS
The Fetch API is a standard part of browser JavaScript but requires more explicit management of CORS (Cross-Origin Resource Sharing) requests. Axios provides more convenient tools for working with CORS, automatically handling headers and providing options for additional configuration.
Community support and popularity
The fetch() is a standard built into modern browsers, so support and questions are usually discussed within the web development community. The Fetch API documentation is extensive and provided by the Mozilla Developer Network (MDN). Some problems and vulnerabilities that users have encountered have either been ignored for a long time or have not been resolved.
Axios has an active repository on GitHub and a large community of users and developers, with many discussions on platforms like Stack Overflow. Discovered problems and vulnerabilities are fixed much faster.
Axios and fetch for web scraping
The subsections above compare the two clients feature by feature. For scraping workloads the practical differences narrow down to four areas.
Setting headers and User-Agent
Both clients accept custom headers. The difference is where you put them.
With fetch, headers go per-request or into a shared options object:
const baseHeaders = {
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'accept-language': 'en-US,en;q=0.9',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
};
const res = await fetch('https://books.toscrape.com', { headers: baseHeaders });With axios.create(), headers set at the instance level apply to every request that instance makes:
import axios from 'axios';
const client = axios.create({
headers: {
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'accept-language': 'en-US,en;q=0.9',
},
timeout: 10000,
});
const res = await client.get('https://books.toscrape.com');For a multi-target scraper where headers rotate per request, the axios interceptor is less code than maintaining a wrapper for fetch.
Proxy configuration
Built-in fetch in Node has no native proxy argument. Routing requests through a proxy requires undici.ProxyAgent, which ships with Node 22 but is not exposed on globalThis.fetch directly:
import { fetch, ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent('http://user:pass@proxy.host:8080');
const res = await fetch('https://books.toscrape.com', { dispatcher });With axios, proxy configuration is one object in axios.create():
import axios from 'axios';
const client = axios.create({
proxy: {
host: 'proxy.host',
port: 8080,
auth: { username: 'user', password: 'pass' },
},
timeout: 10000,
});For rotating proxies, the axios interceptor is the natural place to swap the endpoint per request:
client.interceptors.request.use(config => {
const proxy = proxyPool.next();
config.proxy = { host: proxy.host, port: proxy.port, auth: proxy.auth };
return config;
});The equivalent in fetch requires rebuilding a ProxyAgent on every request or wrapping every call site.
Retries on failure
Scraping at scale hits 429s, 503s, and network resets. Both clients need retry logic. The amount of code differs.
A fetch retry wrapper with timeout and exponential backoff:
async function fetchWithRetry(url, options = {}, { retries = 3, backoff = 500, timeout = 10000 } = {}) {
for (let attempt = 0; attempt <= retries; attempt++) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(id);
if (res.status === 429 || res.status >= 500) throw new Error(`HTTP ${res.status}`);
return res;
} catch (err) {
clearTimeout(id);
if (attempt === retries) throw err;
await new Promise(r => setTimeout(r, backoff * 2 ** attempt));
}
}
}The axios equivalent with axios-retry:
import axios from 'axios';
import axiosRetry from 'axios-retry';
const client = axios.create({ timeout: 10000 });
axiosRetry(client, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: e => e.response?.status === 429 || e.response?.status >= 500,
});The fetch wrapper runs to 18 lines. The axios-retry config is 4. Both produce identical retry behavior.
TLS fingerprint
Neither fetch nor axios configures the TLS handshake with browser-matching cipher suites. Both use Node’s default TLS stack, which presents a different fingerprint than Chrome or Firefox. Servers that inspect TLS before processing the request body see a Node client regardless of the User-Agent header. For static pages where this is a concern, got-scraping configures the TLS handshake with the cipher suite order and extensions typical of browser clients, covered in the JavaScript scraping libraries overview.
When to reach for each
When to use fetch
Built-in fetch covers any one-off GET or POST where you don’t need proxy rotation, structured retry logic, or interceptors. It adds zero dependencies and works identically in Node and the browser. For a simple scraper pulling a few static pages, fetch + Cheerio is the lowest-overhead stack.
When to use Axios
Axios is worth the dependency when you’re building a scraper with a rotating proxy pool, structured retry logic across multiple endpoints, or shared configuration across many request types. The interceptor API centralizes what would otherwise be repeated per-call setup in fetch. For long-running scrapers where connection timeouts, proxy rotation, and error classification matter, axios saves more code than it costs in bundle size.
FAQ
Does fetch in Node.js still need node-fetch?
No. fetch has been built into Node.js since version 18. The node-fetch package was the polyfill for Node < 18 and is no longer needed for Node 18+.
How do I set a timeout with fetch?
fetch has no timeout option. The standard approach is AbortController with setTimeout:
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(id);
} catch (err) {
if (err.name === 'AbortError') console.log('timed out');
else throw err;
}Axios accepts a timeout option directly: axios.create({ timeout: 5000 }).
Does axios handle proxies better than fetch in Node.js?
Axios accepts a proxy object in its config and supports rotation through interceptors. Built-in fetch has no native proxy argument in Node. Routing through a proxy requires undici.ProxyAgent from the undici package, which ships with Node 22 but is not exposed on globalThis.fetch by default.
Which is faster, axios or fetch?
On a local test server with 1,000 requests and Node 22: at low concurrency (c=1), axios runs about 15% faster. At higher concurrency (c=50, c=200), fetch edges ahead by a similar margin. Fetch uses roughly 10 MB less RSS across all concurrency levels. In practice, network latency and server response time dominate the total time, and the difference between the two clients flattens out.


