Node.js has no built-in proxy setting — you attach a proxy agent to your HTTP client. With native fetch (Node 18+) that means an undici ProxyAgent passed as the dispatcher; with axios it means either the proxy config object or an https-proxy-agent. Both take a couple of lines once you know which knob each library exposes.
Native fetch (Node 18+)
Node's built-in fetch ignores the HTTP_PROXY environment variable and has no proxy option. The supported way is an undici ProxyAgent set as the request's dispatcher:
import { ProxyAgent } from "undici";
const agent = new ProxyAgent("http://USER:[email protected]:7777");
const res = await fetch("https://ipinfo.io/ip", { dispatcher: agent });
console.log(await res.text()); // the proxy's exit IPundici ships with Node, so on Node 18+ there is usually nothing extra to install. The credentials go inline in the proxy URL. This is the modern, first-class path — older tutorials that tell you to set HTTP_PROXY for fetch are wrong for native fetch; that only worked for libraries that read the env var.
axios
axios gives you two options. The simplest is the built-in proxy config:
const res = await axios.get("https://example.com", {
proxy: {
host: "pr.quanticdata.io", port: 7777,
auth: { username: "USER", password: "PASS" },
},
});There is a well-known catch: axios's built-in proxy option has historically been unreliable for HTTPS targets through an HTTP proxy (the CONNECT tunnel). The robust alternative most people end up on is an explicit agent:
import { HttpsProxyAgent } from "https-proxy-agent";
const agent = new HttpsProxyAgent("http://USER:[email protected]:7777");
const res = await axios.get("https://example.com", {
httpsAgent: agent, proxy: false, // proxy:false is required!
});The proxy: false line is essential and the number-one axios-proxy bug: without it, axios tries to apply its own proxy handling on top of your agent and the request breaks. Set the agent, disable the built-in proxy, done.
The gotchas across both
| Symptom | Cause | Fix |
|---|---|---|
| fetch ignores the proxy | Set HTTP_PROXY env var expecting fetch to read it | Use an undici ProxyAgent as dispatcher |
| axios HTTPS request hangs/fails | Built-in proxy option on an HTTPS target | Use httpsAgent + proxy: false |
| 407 auth error | Special chars in password not encoded | URL-encode the credentials |
| Works locally, fails in Docker | Env-based proxy libs, wrong env scope | Prefer explicit agents over env vars |
Testing the proxy actually routes
Before pointing either client at your real target, confirm the proxy is in the path — a misconfigured agent silently falls back to your server's IP, and you only find out when the target bans your box. Hit an IP-echo endpoint through the proxy and check the returned address is the proxy's exit, not your machine's. Run it twice against a rotating endpoint and you should see two different IPs, confirming rotation is live; against a sticky one, the same IP twice. If it echoes your own address, the agent isn't wired in — recheck the dispatcher for fetch, or the httpsAgent plus proxy:false pair for axios. This ten-second check is the cheapest insurance against the whole class of scraper-worked-then-everything-got-banned bugs.
Rotation for scraping
Attaching one static proxy is fine for a few requests; scraping at volume needs many IPs. Instead of maintaining and health-checking your own IP list, point the agent at a rotating endpoint — the provider assigns a fresh exit IP per request automatically, so the same one-line agent gives you rotation with no extra code. Use residential IPs for defended targets and datacenter for tolerant, high-volume ones. Reuse the agent across requests (it pools connections) rather than constructing a new one per call — creating a fresh agent per request leaks sockets and slows the whole run. One more practical note for concurrent Node scrapers: cap your concurrency. It is tempting to fire hundreds of requests at once because Node makes it easy, but a rotating pool spreads load across IPs, it does not make a target immune to being hammered in aggregate; a sane concurrency limit plus small jitter between requests keeps the whole operation looking like traffic rather than an attack.
When Node plus a proxy isn't enough
A proxy fixes the IP, but a raw fetch or axios call still only gets you the server-rendered HTML — no JavaScript execution, and you write your own parsing with cheerio or regex. When a target renders client-side or blocks harder than proxies alone can beat, point your Node client at a scraping API instead of the site: one request with the URL, and you get back clean Markdown or structured JSON with proxy, rendering and anti-block handled server-side. Your Node code stays a single fetch or axios.post — no agent, no cheerio, no anti-block maintenance. Keep raw fetch/axios+proxy for simple static targets; reach for the API when the page fights back or you need it at scale. For agent-driven Node apps, the same capabilities are available as MCP tools.