How to Use Proxies in Node.js (Fetch Guide)
A practical Node.js Fetch guide for routing requests through HTTP, HTTPS, and SOCKS proxies with agents, timeouts, retries, and rotation.
To use proxies with Node.js Fetch, you need a proxy-aware HTTP agent because the native Fetch API does not accept a simple proxy option. Install a proxy agent package, pass the agent into your Fetch request, and route traffic through an HTTP, HTTPS, or SOCKS proxy depending on your setup.
Node.js has become one of the most common environments for scraping, testing, automation, API monitoring, and backend data collection. Since modern Node.js includes Fetch support, many developers now reach for fetch() instead of older request libraries.
That makes sense. Fetch is clean, readable, and familiar if you have worked with browser JavaScript.
But there is one important catch: using proxies with Fetch in Node.js is not as simple as adding a proxy field to the request.
If you are coming from Axios, this can feel confusing. Axios has its own proxy configuration style. Fetch is lower-level in this area and usually needs an agent to handle proxy routing.
This guide explains how proxies work with Node.js Fetch, when to use them, how to configure them, and what mistakes cause timeouts, 403 errors, connection failures, and inconsistent results.
Why Use Proxies With Node.js Fetch?
Developers use proxies with Node.js Fetch when they need requests to appear from a different IP address, location, or network path.
That matters for several common workflows.
A scraping script may need to avoid sending every request from the same server IP. A QA team may need to test how a website behaves in different regions. An SEO team may need to check local search results. A monitoring tool may need to verify content from multiple locations. A price intelligence workflow may need to collect public data without overloading one source IP.
In these cases, the proxy is not just a privacy tool. It is part of the request infrastructure.
Instead of connecting directly to a website, your Fetch request goes through a proxy server first. The target website sees the proxy IP address, not the origin server running your Node.js script.
That is useful, but only when the proxy is stable. Weak proxies lead to failed requests, slow responses, and blocked sessions. For production workflows, affordable working proxies are more practical than free proxy lists that disappear, leak traffic, or return inconsistent results.
Does Node.js Fetch Support Proxies?
Native Node.js Fetch does not support proxies through a simple built-in proxy option.
This is the part that trips people up.
You cannot usually write something like this and expect it to work:
await fetch("https://example.com", {
proxy: "http://username:password@proxyhost:port"
});That pattern may look logical, but Fetch does not work that way.
Instead, Node.js Fetch needs a dispatcher or agent that knows how to route traffic through a proxy. Depending on your Node version and setup, developers commonly use packages such as undici, proxy-agent, http-proxy-agent, https-proxy-agent, or socks-proxy-agent.
The exact choice depends on whether your proxy is HTTP, HTTPS, or SOCKS.
Basic Proxy Setup With Fetch
A common approach is to use a proxy agent package.
Example using undici with a proxy agent:
import { fetch, ProxyAgent } from "undici";
const proxyAgent = new ProxyAgent("http://username:password@proxy.example.com:8080");
const response = await fetch("https://httpbin.org/ip", {
dispatcher: proxyAgent
});
const data = await response.json();
console.log(data);In this example, the request goes through the proxy server before reaching the target URL.
The key part is this:
dispatcher: proxyAgentThat tells Fetch how to route the request.
Without the proxy agent, Fetch sends the request directly from your server.
Using Environment Variables for Proxy Credentials
Hardcoding proxy credentials inside your script is a bad habit.
A cleaner pattern is to store the proxy URL in an environment variable:
PROXY_URL=http://username:password@proxy.example.com:8080Then load it inside Node.js:
import { fetch, ProxyAgent } from "undici";
const proxyUrl = process.env.PROXY_URL;
if (!proxyUrl) {
throw new Error("Missing PROXY_URL environment variable");
}
const proxyAgent = new ProxyAgent(proxyUrl);
const response = await fetch("https://httpbin.org/ip", {
dispatcher: proxyAgent
});
console.log(await response.json());This keeps credentials out of your codebase and makes it easier to switch proxies between local, staging, and production environments.
For larger workflows, you can store multiple proxy URLs and rotate through them based on task, domain, location, or retry status.
HTTP vs HTTPS Proxies in Node.js Fetch
Proxy terminology can be confusing because there are two different URLs involved.
There is the target URL, such as:
https://example.comAnd there is the proxy URL, such as:
http://username:password@proxyhost:8080Many HTTPS requests still use an HTTP proxy URL. That does not automatically mean the target traffic is unencrypted. HTTPS traffic usually uses the CONNECT method through the proxy, allowing the secure connection to the destination site.
Get ProxiesThatWork’s stories in your inbox
Join Medium for free to get updates from this writer.
What matters is whether your agent supports the proxy type and whether your provider gives the correct endpoint format.
Common proxy formats include:
http://user:pass@host:port
https://user:pass@host:port
socks5://user:pass@host:portUse the format your proxy provider gives you.
If your proxy is SOCKS5, use a SOCKS-compatible agent rather than assuming an HTTP proxy agent will work.
Rotating Proxies With Fetch
For scraping, monitoring, and automation, one proxy is often not enough.
A simple rotation pattern looks like this:
import { fetch, ProxyAgent } from "undici";
const proxies = [
"http://user:pass@proxy1.example.com:8000",
"http://user:pass@proxy2.example.com:8000",
"http://user:pass@proxy3.example.com:8000"
];
function getRandomProxy() {
return proxies[Math.floor(Math.random() * proxies.length)];
}
async function fetchWithProxy(url) {
const proxyAgent = new ProxyAgent(getRandomProxy());
const response = await fetch(url, {
dispatcher: proxyAgent
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.text();
}
const html = await fetchWithProxy("https://example.com");
console.log(html.slice(0, 200));This is a basic example. It works for small scripts, but production rotation needs more control.
Random rotation can cause problems if the target website expects session consistency. Login flows, carts, dashboards, and multi-step forms may break if the IP changes too often.
For session-based workflows, use sticky proxies or assign one proxy per session.
Adding Timeouts
Fetch requests can hang if the proxy is slow, overloaded, or unavailable.
Use AbortController to set a timeout:
import { fetch, ProxyAgent } from "undici";
async function fetchWithTimeout(url, proxyUrl, timeoutMs = 15000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const proxyAgent = new ProxyAgent(proxyUrl);
const response = await fetch(url, {
dispatcher: proxyAgent,
signal: controller.signal
});
return response;
} finally {
clearTimeout(timeout);
}
}
try {
const response = await fetchWithTimeout(
"https://httpbin.org/ip",
process.env.PROXY_URL
);
console.log(await response.text());
} catch (error) {
console.error("Request failed:", error.message);
}Timeouts are important because proxy failures are not always immediate. A poor proxy may stall instead of returning a clean error.
Without timeout control, your script can freeze, slow down a queue, or waste worker capacity.
Handling 403, 429, and 5xx Responses
A proxy connection can succeed while the request itself fails.
That means you need to distinguish network errors from HTTP responses.
A 403 response usually means forbidden. The target may dislike the IP, headers, cookies, behavior, or access pattern.
A 429 response means too many requests. You may be sending requests too quickly, reusing the same IP too often, or missing backoff logic.
A 5xx response means the server had a problem, but repeated 5xx errors can also happen when your request pattern triggers unstable routing or backend defenses.
A simple retry strategy might look like this:
async function retryFetch(url, proxyUrl, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetchWithTimeout(url, proxyUrl);
if (response.status === 429 || response.status >= 500) {
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
continue;
}
if (response.status === 403) {
throw new Error("Blocked with 403");
}
return response;
} catch (error) {
if (attempt === maxRetries) throw error;
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
}
}
}This is intentionally simple. Real retry logic should track proxy health, target domain, status codes, response time, and session state.
Fetch vs Axios for Proxies
Fetch is clean and native, but Axios remains popular because it has a mature ecosystem and familiar proxy configuration patterns.
If you already use Axios, you may prefer its request interceptors, response handling, and broader examples. If you want to compare both approaches, this guide on using proxies in Node.js with Axios is the natural next read.
The practical difference is this:
Fetch is lightweight and modern.
Axios is feature-rich and convenient.
Both can work with proxies, but both need correct configuration.
Choose Fetch if you want native-style requests and fewer dependencies. Choose Axios if your project already depends on it or you want its request handling features.
Common Proxy Mistakes With Node.js Fetch
The first mistake is assuming Fetch has a built-in proxy option. It does not work like that in most Node.js setups.
The second mistake is using the wrong agent for the proxy type. HTTP, HTTPS, and SOCKS proxies need compatible handling.
The third mistake is rotating proxies too aggressively. If a website expects a stable session, constant IP changes can trigger blocks.
The fourth mistake is ignoring timeouts. Proxies fail in messy ways, and your script needs to recover.
The fifth mistake is treating every failure as a proxy problem. Sometimes the issue is headers, cookies, TLS fingerprinting, request frequency, JavaScript rendering, or blocked user agents.
Good proxy infrastructure helps, but it does not replace responsible request behavior.
Final Takeaway
Using proxies with Node.js Fetch requires a proxy-aware dispatcher or agent. You cannot rely on a simple proxy option.
For small scripts, one proxy agent may be enough. For scraping or automation, you need rotation, timeouts, retries, session control, and proxy health tracking.
Fetch is a strong choice for modern Node.js workflows, but proxy success depends on configuration quality and IP reliability.
Start simple, test against an IP-check endpoint, add timeout handling, then build rotation only when your workflow actually needs it.









