Back to all posts

Axios vs fetch in Node.js

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.

ParameterFetchAxios
InstallationBuilt-in in most modern browsersRequires installation through NPM/Yarn
Browser CompatibilitySupported by most modern browsers, but requires polyfills for IESupported by all modern browsers and IE
JSON HandlingRequires calling .json() on the responseAutomatically transforms JSON to/from objects
Error HandlingDoes not reject promises for HTTP error responsesAutomatically rejects promises for HTTP error responses
TimeoutsNo built-in supportSupports timeouts out of the box
Download ProgressNot natively supportedSupports download progress tracking
CORS (Cross-Origin Resource Sharing)SupportedSupported
CSRF/XSRF ProtectionNo built-in supportBuilt-in support
InterceptorsNot supportedSupported
Default ConfigurationLimited supportEasily configurable global settings
Node.js UsageBuilt in since Node 18Supported out of the box
Library SizeSmaller (built into browsers by default)Larger (due to additional features)
Promises & async/awaitEasily used with promises and async/awaitRequires chaining with .then() or using async/await
Built-in MethodsDirectly uses browser’s built-in methods (Headers, Request, Response)Requires axios.get(), axios.post(), etc.
Global SettingsUtilizes browser’s global settings (e.g., headers, caching)Easily configurable global settings
Convenience in BrowserNative browser API, simplifying debugging in DevToolsMay require source maps for debugging in DevTools
TechnologyES6+ standardLibrary, 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:

ClientConcurrencyTime (ms)req/sPeak RSS (MB)
fetch11,25879588
axios11,09291699
fetch108431,186102
axios107801,282109
fetch507001,428118
axios507441,344121
fetch2008641,158128
axios2009541,049139

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.

Line chart showing throughput in req/s for fetch and axios at concurrency levels 1, 10, 50, and 200. axios leads at c=1 (916 vs 795 req/s) and c=10 (1,282 vs 1,186). fetch peaks at c=50 (1,428 vs 1,344 for axios) and stays ahead at c=200 (1,158 vs 1,049). The crossover falls between c=10 and c=50.

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.

Valentina Skakun
Valentina Skakun
Valentina is a software engineer who builds data extraction tools before writing about them. With a strong background in Python, she also leverages her experience in JavaScript, PHP, R, and Ruby to reverse-engineer complex web architectures.If data renders in a browser, she will find a way to script its extraction.
Articles

Might Be Interesting