Proxies in Node.js: use agents
Node.js HTTP clients route traffic through proxies using agents: objects that control how connections are made. The most reliable approach across libraries is to create a proxy agent and pass it to your client. This guide covers axios, native fetch (powered by undici) and got. For browser automation in Node, see Puppeteer proxies.
Axios
Axios has a built-in proxy option, but it has historically had problems with HTTPS targets through HTTP proxies. The dependable pattern is to disable the built-in option and use https-proxy-agent:
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = new HttpsProxyAgent('http://USER:PASS@gate.example.com:7777');
const { data } = await axios.get('https://httpbin.org/ip', {
httpsAgent: agent,
httpAgent: agent,
proxy: false,
timeout: 30000,
});
console.log(data);For a reusable instance:
const client = axios.create({ httpsAgent: agent, httpAgent: agent, proxy: false, timeout: 30000 });Native fetch (Node 22 and later)
Node's built-in fetch is implemented by undici. Use undici's ProxyAgent as a dispatcher:
import { ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent('http://USER:PASS@gate.example.com:7777');
const res = await fetch('https://httpbin.org/ip', { dispatcher });
console.log(await res.json());If your Node version's global fetch doesn't accept the dispatcher, import fetch from undici directly. You can also set a global dispatcher with setGlobalDispatcher(new ProxyAgent(...)). Node 20 reached end of life on 30 April 2026, so build new projects on Node 22 or 24, which are the supported LTS lines as of October 2026.
got
import got from 'got';
import { HttpsProxyAgent } from 'hpagent';
const res = await got('https://httpbin.org/ip', {
agent: {
https: new HttpsProxyAgent({ proxy: 'http://USER:PASS@gate.example.com:7777' }),
},
timeout: { request: 30000 },
}).json();
console.log(res);SOCKS5
Use socks-proxy-agent with axios or got:
import { SocksProxyAgent } from 'socks-proxy-agent';
const agent = new SocksProxyAgent('socks5h://USER:PASS@socks.example.com:1080');
const { data } = await axios.get('https://httpbin.org/ip', { httpsAgent: agent, httpAgent: agent, proxy: false });socks5h resolves DNS through the proxy. See SOCKS5 proxies.
Rotating proxies
Create a new agent per session:
const newAgent = () =>
new HttpsProxyAgent(`http://customer-USER-country-us-session-${Math.random().toString(36).slice(2, 10)}:PASS@gate.example.com:7777`);
async function fetchWithFreshIp(url) {
const agent = newAgent();
return axios.get(url, { httpsAgent: agent, httpAgent: agent, proxy: false, timeout: 30000 });
}Agents keep connections alive, so reusing one agent with a rotating gateway may keep you on the same exit. A new agent per session avoids that. See proxy rotation strategies.
Retries
async function getWithRetry(url, attempts = 4) {
for (let n = 0; n < attempts; n++) {
try {
const res = await fetchWithFreshIp(url);
if (res.status === 200) return res.data;
} catch (e) {
if (n === attempts - 1) throw e;
}
await new Promise((r) => setTimeout(r, 1000 * 2 ** n));
}
}Environment variables
Some tools respect HTTP_PROXY/HTTPS_PROXY, but Node's core HTTP modules historically ignored them. That has changed: Node 24.5 and later (and 22.21 onwards) can read HTTP_PROXY, HTTPS_PROXY and NO_PROXY for both fetch and the http/https modules when you opt in with NODE_USE_ENV_PROXY=1 or the --use-env-proxy flag. It's off by default, so don't assume a library picks the variables up unless its documentation says so. See the section on built-in proxy support below.
Common errors
| Error | Cause |
|---|---|
407 Proxy Authentication Required | Wrong credentials, or special characters not URL-encoded |
ECONNREFUSED | Wrong proxy host/port |
ECONNRESET / socket hang up | Proxy dropped connection; rotate and retry |
EPROTO / TLS errors | Using axios's built-in proxy with HTTPS; switch to an agent |
ETIMEDOUT | Slow exit; increase timeout or rotate |
See common proxy errors.
Proxy tip: Want to skip proxy management on some targets? Geonode offers both raw proxies and a Scraper API that returns HTML or Markdown with optional JavaScript rendering. We compare the two approaches in using Geonode with Node.js.
Choosing a Node.js HTTP client for proxy work
| Client | Proxy approach | Notes |
|---|---|---|
| Native fetch (undici) | ProxyAgent dispatcher | No extra HTTP dependency; modern and fast |
| axios | https-proxy-agent + proxy: false | Very popular; interceptors are handy |
| got | hpagent agents | Built-in retries and hooks |
| undici request | ProxyAgent | Lowest-level, highest performance |
For new projects, native fetch with undici's ProxyAgent is a clean default.
Concurrency with p-limit
Control concurrency so proxies and targets aren't overwhelmed:
import pLimit from 'p-limit';
import { ProxyAgent } from 'undici';
const limit = pLimit(10);
const session = () => `customer-USER-country-us-session-${Math.random().toString(36).slice(2, 10)}`;
async function fetchPage(url) {
const dispatcher = new ProxyAgent(`http://${session()}:PASS@gate.example.com:7777`);
try {
const res = await fetch(url, { dispatcher, headers: { 'Accept-Language': 'en-US,en;q=0.9' } });
return { url, status: res.status, body: await res.text() };
} finally {
await dispatcher.close();
}
}
const results = await Promise.all(urls.map((u) => limit(() => fetchPage(u))));Closing each dispatcher releases its connections; a new dispatcher per request guarantees a new session.
TLS fingerprints in Node
Node's TLS fingerprint differs from browsers. For sites that check it, consider:
- Using Puppeteer or Playwright for those targets. See Puppeteer proxies.
- Routing them through a web unblocker.
Debugging tips
- Log
res.statusand the first part of the body to detect block pages. - Test the same request with curl to isolate proxy vs code issues. See curl proxy.
- Watch for unhandled promise rejections hiding proxy errors.
- Set timeouts explicitly; defaults can be long.
For general patterns, see web scraping proxies.
Handling proxy rotation with axios interceptors
Interceptors let you attach a fresh proxy agent to every request automatically:
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
const client = axios.create({ proxy: false, timeout: 30000 });
client.interceptors.request.use((config) => {
const sid = Math.random().toString(36).slice(2, 10);
const agent = new HttpsProxyAgent(`http://customer-USER-country-us-session-${sid}:${process.env.PROXY_PASS}@gate.example.com:7777`);
config.httpsAgent = agent;
config.httpAgent = agent;
return config;
});
client.interceptors.response.use(undefined, async (error) => {
const cfg = error.config;
cfg.__retries = (cfg.__retries || 0) + 1;
if (cfg.__retries <= 3) return client(cfg); // new agent assigned by the request interceptor
throw error;
});Each retry passes through the request interceptor again and gets a new session, so retries use fresh IPs.
Handling cookies through a proxy
Node HTTP clients don't keep cookies by default. For sessions that need them, use a cookie jar (for example tough-cookie with an axios cookie-jar integration) and keep the same proxy session for the whole flow. Rotating the IP while keeping cookies can look like session hijacking. See sticky vs rotating sessions.
Serverless environments
On serverless platforms, outbound IPs change frequently, so IP whitelisting won't work; use username/password authentication. Cold starts mean agents are created often, which is fine for rotation but adds a little latency. Keep proxy credentials in the platform's secret store. See proxy authentication methods.
Node's built-in proxy support
Recent Node.js releases can route requests through a proxy without any extra package. Since Node 24.5 (and 22.21 on the older line), built-in proxy support covers both fetch() and the http/https modules, configured from the standard environment variables:
NODE_USE_ENV_PROXY=1 \
HTTPS_PROXY=http://USER:PASS@gate.example.com:7777 \
NO_PROXY=localhost,127.0.0.1 \
node scraper.jsOr use the flag instead of the variable: node --use-env-proxy scraper.js. Newer releases (24.14 and 25.4 onwards) also add http.setGlobalProxyFromEnv() to switch it on from code, and you can give individual agents their own settings with the proxyEnv option.
Things to know:
- It's opt-in. Setting
HTTPS_PROXYalone still does nothing in Node unless the flag or variable is set. - Credentials go in the URL, URL-encoded if they contain special characters.
- It's one proxy per process (or per agent). For per-request rotation, keep using undici's
ProxyAgentorhttps-proxy-agentas shown above. - SOCKS isn't covered; use
socks-proxy-agentfor that.
The details, including NO_PROXY syntax, are in Node's built-in proxy support documentation. Node 20 reached end of life in April 2026; Node's release schedule shows which versions are supported. For other languages, see Python requests proxies and curl proxy.
Key takeaways
- Node.js clients use proxy agents: undici
ProxyAgentfor fetch,https-proxy-agentfor axios,hpagentfor got. - With axios, disable the built-in
proxyoption when using an agent. - Create a new agent per session to guarantee rotation with backconnect gateways.
- Use
socks-proxy-agentwithsocks5hfor SOCKS proxies. - Control concurrency with p-limit and set explicit timeouts. See web scraping proxies.
FAQ
Why doesn't axios's proxy option work with HTTPS?
Historically it sent requests in a way some proxies and HTTPS targets didn't handle well. Using https-proxy-agent with proxy: false avoids the issue.
Which proxies should I use for Node.js scraping?
The same principles apply as in any language. See best proxies for web scraping.
Editorial note: Proxy providers change their pools, features and prices often, so treat any figures here as typical ranges at the time of writing and confirm current terms with the provider before you buy.