Back to Blog
Guides
Mihnea-Octavian ManolacheLast updated on May 1, 202611 min read

How to Use a Proxy in Node-Fetch: A Practical Guide

How to Use a Proxy in Node-Fetch: A Practical Guide
TL;DR: Node-Fetch has no built-in proxy switch, so you wire an HTTP, HTTPS, or SOCKS5 agent into the request through its agent option. This guide walks through how to use a proxy in Node-Fetch end to end: authenticated HTTP and HTTPS proxies, SOCKS5, rotation, retries, TLS edge cases, troubleshooting, and the modern undici route for Node 18+ native fetch.

If you have ever stared at a 403 from a target you used to scrape happily, you already know why this article exists. Learning how to use a proxy in Node-Fetch is the difference between a script that works on your laptop and one that survives in CI on a different IP, in a different country, against a real anti-bot stack. The good news: how to use a proxy in Node-Fetch boils down to one small API surface, and the rest is operational glue.

Node-Fetch is a popular HTTP client for Node.js that brings the browser's window.fetch style to the server. It is small, async, and pleasant to use, but it intentionally does not ship a proxy option. Instead, it exposes an agent slot, and you plug an external proxy agent into it. That single design choice is the mechanism behind every recipe below.

This guide is provider-neutral and code-first. You will set up an HTTP/HTTPS proxy, send your first proxied request, add credentials safely, switch to SOCKS5, rotate through a pool, add timeouts and retries, and verify traffic is actually leaving through the proxy. We will also cover the Node 18+ alternative using undici's ProxyAgent, plus a troubleshooting matrix for the errors you will hit on day one.

How to use a proxy in Node-Fetch: why you need an agent

Node-Fetch does not have a built-in proxy parameter. To route requests through a proxy, you pass a Node.js http.Agent (or https.Agent) implementation to the agent option on each fetch() call. The community packages https-proxy-agent and socks-proxy-agent implement that interface and tunnel your traffic through the proxy you point them at.

The takeaway: the only Node-Fetch-specific knowledge you need is that the agent option exists and accepts any compliant agent. Everything else, HTTP, HTTPS, SOCKS5, authentication, TLS quirks, rotation, lives in the agent layer. That keeps your fetch code identical across providers and lets you swap the transport without rewriting business logic.

Project setup and dependencies

You need a recent Node.js LTS. The node-fetch README pins an exact minimum version that has shifted across releases, so check it against the official node-fetch repository before you lock your CI matrix. Anything on a current LTS line is normally safe.

Pick a node-fetch major intentionally. Version 3 is ESM-only, which means you set "type": "module" in your package.json (or use .mjs files) and load it with import. Version 2 still works in CommonJS projects and is interchangeable for proxy purposes. The two versions are largely the same in behaviour, so v2 is a perfectly reasonable choice if your codebase is not yet on ESM.

Install the proxy agent alongside Node-Fetch:

# CommonJS friendly
npm install node-fetch@2 https-proxy-agent

# ESM (requires "type": "module" in package.json)
npm install node-fetch https-proxy-agent

Sending the first request through an HTTP proxy

Once the packages are in place, the recipe is mechanical: build a proxy URL, hand it to HttpsProxyAgent, and pass that agent to fetch(). The same agent class works whether your target is http:// or https://, because it tunnels HTTPS through the proxy with CONNECT.

// proxy-fetch.js (CommonJS, node-fetch v2)
const fetch = require('node-fetch');
const { HttpsProxyAgent } = require('https-proxy-agent');

async function main() {
  const proxyUrl = `http://${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`;
  const agent = new HttpsProxyAgent(proxyUrl);

  const res = await fetch('https://ifconfig.me/all.json', { agent });
  const body = await res.json();
  console.log('Outbound IP:', body.ip_addr);
}

main().catch(console.error);

A few details that quietly matter when you ship this:

  • Use a destructured import ({ HttpsProxyAgent }) for https-proxy-agent v6 and above. The default export shape changed across majors, and the wrong import is a common cause of undefined is not a constructor.
  • Reuse the agent across requests that target the same proxy. Constructing a fresh agent per call works, but you lose connection pooling and pay a TLS handshake every time.
  • Hit a known IP-echo endpoint first (ifconfig.me, ident.me, or ipinfo.io/json). If you cannot see the proxy's IP coming back, do not move on; nothing else will work until that base case does.

Authenticating against a proxy server

Most paid proxies require credentials. The convention is to put them in the URL with the http://USERNAME:PASSWORD@HOST:PORT shape, which https-proxy-agent parses for you:

const user = encodeURIComponent(process.env.PROXY_USER);
const pass = encodeURIComponent(process.env.PROXY_PASS);
const proxyUrl = `http://${user}:${pass}@${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`;
const agent = new HttpsProxyAgent(proxyUrl);

Two things bite people here. First, hard-coding credentials into source files leaks them through Git history; keep them in environment variables (or a secrets manager) and inject them at runtime. Second, special characters in passwords (@, :, /, #) corrupt the URL parse silently, and you will get a misleading 407 Proxy Authentication Required instead of a parse error. Wrapping the username and password in encodeURIComponent() removes that whole class of bug.

Using SOCKS5 proxies with Node-Fetch

When your provider hands you a SOCKS5 endpoint, swap the agent. Node-Fetch does not care about the protocol; it just calls into whatever agent you give it. Install socks-proxy-agent:

npm install socks-proxy-agent
const fetch = require('node-fetch');
const { SocksProxyAgent } = require('socks-proxy-agent');

// socks5h:// resolves DNS through the proxy; socks5:// resolves locally.
const proxyUrl = `socks5h://${process.env.PROXY_USER}:${process.env.PROXY_PASS}` +
                 `@${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`;
const agent = new SocksProxyAgent(proxyUrl);

fetch('https://ifconfig.me/all.json', { agent })
  .then(r => r.json())
  .then(console.log);

Prefer the socks5h:// scheme when you scrape, because it forwards DNS resolution through the proxy. The plain socks5:// scheme resolves the hostname on your machine, which leaks your real DNS and partially defeats the point of routing through the proxy in the first place.

Working with HTTPS targets and self-signed certificates

Some proxy products, especially the interception-style "web unblocker" services, present their own TLS certificate to your client and re-sign the upstream response. Node will reject that handshake by default with UNABLE_TO_VERIFY_LEAF_SIGNATURE or SELF_SIGNED_CERT_IN_CHAIN.

The lazy fix is to set process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'. Do not do that in production. It disables certificate verification for every outbound request in the process, including the ones you very much want verified.

Scope it instead to the agent itself:

const agent = new HttpsProxyAgent(proxyUrl, { rejectUnauthorized: false });

That keeps the rest of your application's TLS posture intact. The exact constructor option name and how it propagates has shifted across https-proxy-agent majors, so re-check the package's npm page when you upgrade, and avoid this flag entirely if you can pin a CA bundle instead.

Rotating proxies for resilient scraping

Many sites use IP-based rate limiting and bot detection, so a single proxy IP will get throttled or banned the moment you scale. Rotating across a pool spreads the load and makes each request look like it comes from a different user. There are two patterns worth knowing for how to use a proxy in Node-Fetch at scale: a random pick per request, and a deterministic round-robin walk. Both build on the same agent-per-proxy idea you already have.

Picking a random proxy on each request

When the only thing you care about is dispersion, randomly index into the pool and instantiate a fresh agent per call:

const proxies = process.env.PROXY_POOL.split(','); // host:port,host:port,...

function randomAgent() {
  const pick = proxies[Math.floor(Math.random() * proxies.length)];
  return new HttpsProxyAgent(`http://${pick}`);
}

await fetch(targetUrl, { agent: randomAgent() });

If the same proxy serves several consecutive requests, cache its agent in a Map keyed by proxy URL so you keep connection reuse.

Iterating sequentially through a proxy list

For reproducible debugging or simple round-robin behaviour, walk the list with a counter. Sequential rotation also makes per-proxy success metrics easy to attribute, which matters once you start retiring dead proxies:

let i = 0;
for (const url of urls) {
  const agent = new HttpsProxyAgent(`http://${proxies[i % proxies.length]}`);
  await fetch(url, { agent });
  i++;
}

Adding retries, timeouts, and error handling

Production traffic over public proxies fails in interesting ways: hangs, half-open sockets, transient ECONNRESET, sudden 5xx storms. A robust setup combines a per-request timeout, a bounded retry loop with backoff, and a circuit breaker that retires a proxy after repeated failures. (More on rotation strategy in our deeper guide on rotating proxies for web scraping.)

async function fetchWithProxy(url, getAgent, opts = {}) {
  const { tries = 3, timeoutMs = 10_000 } = opts;
  let lastErr;

  for (let attempt = 1; attempt <= tries; attempt++) {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), timeoutMs);
    try {
      const res = await fetch(url, { agent: getAgent(), signal: ctrl.signal });
      if (res.ok) return res;
      if (res.status === 407 || res.status === 403) throw new Error(`status ${res.status}`);
    } catch (err) {
      lastErr = err;
      const backoff = 2 ** attempt * 250 + Math.random() * 250;
      await new Promise(r => setTimeout(r, backoff));
    } finally {
      clearTimeout(t);
    }
  }
  throw lastErr ?? new Error('exhausted retries');
}

Track failure counts per proxy URL in a small object, and when one crosses a threshold (three failures in a minute, for example), drop it from the pool until a cool-down passes. That single rule is the part most tutorials skip when they explain how to use a proxy in Node-Fetch, and it is what keeps a handful of dead IPs from poisoning every retry. Pair it with AbortController so a hung proxy never wedges your worker forever.

Verifying the proxy is actually being used

Never trust a proxy you have not verified. The cheapest test is a diff: hit an IP/geo echo endpoint twice, once with the agent and once without, and compare. The IP and country must change.

const direct = await fetch('https://ipinfo.io/json').then(r => r.json());
const proxied = await fetch('https://ipinfo.io/json', { agent }).then(r => r.json());
console.log('direct  :', direct.ip, direct.country);
console.log('proxied :', proxied.ip, proxied.country);

If the two lines match, your agent is being ignored, usually because of an import shape mistake or a misspelled option key. Add this check to your scraper's startup probe so a silently-direct deployment fails loud.

Native fetch in Node 18+ vs Node-Fetch

Node 18 ships a global fetch powered by undici, and many teams have moved off the node-fetch package entirely. The catch: built-in fetch does not accept the agent option, so https-proxy-agent will not plug in directly. The native equivalent is undici's ProxyAgent, set as the global dispatcher:

import { ProxyAgent, setGlobalDispatcher } from 'undici';

setGlobalDispatcher(new ProxyAgent(process.env.PROXY_URL));

const res = await fetch('https://ifconfig.me/all.json');
console.log(await res.json());

The undici proxy API has shifted across releases (auth in the URL, custom headers, request-scoped dispatchers), so check the current undici docs before you lock anything down. The mental model still maps cleanly to how to use a proxy in Node-Fetch, just at the dispatcher layer instead of per-request.

Troubleshooting common Node-Fetch proxy errors

Most node-fetch proxy bugs collapse into a small matrix:

Symptom

Likely cause

Fix

ECONNREFUSED

Wrong proxy host or port, or the proxy is down.

Telnet/nc the host:port; rotate to another proxy.

ETIMEDOUT / hung request

No AbortController deadline, or proxy is silently dropping packets.

Wrap each fetch with a per-request timeout and retry on a different proxy.

407 Proxy Authentication Required

Missing or URL-corrupted credentials.

URL-encode user/pass; verify env vars are loaded.

SELF_SIGNED_CERT_IN_CHAIN

Proxy presents its own TLS cert.

Set rejectUnauthorized: false on the agent (scoped), not the global env var.

Cannot find module 'node-fetch'

v3 ESM imported from a CommonJS file.

Add "type": "module" or downgrade to node-fetch@2.

HttpsProxyAgent is not a constructor

Wrong import shape for v6+.

Use const { HttpsProxyAgent } = require('https-proxy-agent').

When a target keeps failing across multiple healthy proxies, the bottleneck is usually anti-bot fingerprinting rather than the proxy layer. At that point, no amount of refining how to use a proxy in Node-Fetch will help; you need a request layer that handles fingerprinting too.

Key takeaways and next steps

The mechanism behind how to use a proxy in Node-Fetch is one option: agent. Pick the right agent class (HttpsProxyAgent for HTTP/HTTPS, SocksProxyAgent for SOCKS5), feed it a properly encoded URL, and the rest is rotation, retries, and verification. From here, the natural next steps are layering rotation logic and comparing alternative HTTP clients for Node.js.

Key Takeaways

  • Node-Fetch has no proxy option; you wire support in by passing an Agent (from https-proxy-agent or socks-proxy-agent) to the agent field on fetch().
  • Pick the major deliberately: node-fetch@3 is ESM-only and needs "type": "module", while node-fetch@2 is the CommonJS-friendly choice with the same proxy behaviour.
  • Always URL-encode credentials and read them from environment variables. A 407 Proxy Authentication Required is more often a parse bug than a real auth failure.
  • Production-grade Node-Fetch proxy code combines AbortController timeouts, exponential backoff, and dropping a dead proxy after a few failures, not just a try/catch around fetch.
  • On Node 18+ with native fetch, the agent option does not work; use undici's ProxyAgent plus setGlobalDispatcher instead, and re-check the API surface against the current undici docs.

FAQ

Does Node-Fetch v3 support proxies natively?

No. Neither v2 nor v3 of Node-Fetch implements a proxy option. The proxy mechanism is identical across both majors: install https-proxy-agent (or socks-proxy-agent), construct an agent from your proxy URL, and pass it to fetch() via the agent parameter. The only v2-vs-v3 difference is the module system, ESM in v3 versus CommonJS in v2.

Can I reuse the same HttpsProxyAgent across many fetch calls?

Yes, and you should. Reusing one agent per (proxyUrl, target host) pair lets the underlying socket pool keep connections alive, which removes a TLS handshake from every request and noticeably reduces latency under load. Only build a new agent when the proxy URL itself changes, for example during rotation. Cache them in a Map keyed by proxy URL to keep both reuse and rotation.

What is the difference between HttpProxyAgent and HttpsProxyAgent, and when do I need each?

HttpProxyAgent tunnels plain http:// requests through a proxy without CONNECT. HttpsProxyAgent issues an HTTP CONNECT to the proxy and then runs TLS to the target, which is what you need for any https:// URL. In practice, HttpsProxyAgent covers both protocols safely and is the default recommendation. Reach for HttpProxyAgent only when you have a specific reason and target HTTP-only endpoints.

Why does my proxied Node-Fetch request return 407 Proxy Authentication Required?

A 407 means the proxy did not accept your credentials, but the cause is usually upstream of the proxy itself. The most common one is a special character in the password breaking the URL parse. Wrap both fields in encodeURIComponent and reload them from environment variables. After that, double-check the credential format your provider expects (some require a session token, country code, or session prefix in the username).

How do I use a proxy with the built-in fetch in Node.js 18+ instead of node-fetch?

The native fetch in Node 18+ uses undici, which ignores the agent option. Install undici, build a ProxyAgent from your proxy URL, and register it with setGlobalDispatcher. After that, every plain fetch() call routes through the proxy with no further code changes. The undici API has evolved across releases, so verify the current option names against undici's documentation before pinning.

Conclusion

Once you internalise the agent option, every recipe in this guide is a small variation on the same idea. HTTP, HTTPS, and SOCKS5 share one fetch signature; auth is URL encoding done correctly; rotation, retries, and dead-proxy skipping live in a thin wrapper around fetch(); and the Node 18+ undici route is the same mental model translated to a global dispatcher.

Do not underestimate the operational layer. Free proxies are useless in production: low IP reputation, high downtime, and unpredictable TLS behaviour will eat your error budget. A clean residential or datacenter pool, plus the timeout-and-retry wrapper from earlier, is what turns a snippet into a deployable scraper.

If you would rather not run the proxy plumbing yourself, the Scraper API from WebScrapingAPI handles proxy rotation, anti-bot mitigation, and retries behind a single endpoint, so you can keep your node-fetch code and swap the request layer. From here, the natural next moves are layering rotation strategy and choosing the right Node.js HTTP client for your workload, both of which our companion guides cover in depth.

About the Author
Mihnea-Octavian Manolache, Full Stack Developer @ WebScrapingAPI
Mihnea-Octavian ManolacheFull Stack Developer

Mihnea-Octavian Manolache is a Full Stack and DevOps Engineer at WebScrapingAPI, building product features and maintaining the infrastructure that keeps the platform running smoothly.

Start Building

Ready to Scale Your Data Collection?

Join 2,000+ companies using WebScrapingAPI to extract web data at enterprise scale with zero infrastructure overhead.