Native fetch needs an Undici dispatcher
Node fetch is powered by Undici. Old http.Agent proxy packages do not automatically affect it. Give fetch a dispatcher built for the proxy you want to use.
import { fetch, ProxyAgent } from 'undici';
const proxy = new ProxyAgent('http://USER:PASS@proxynade.net:2555');
const res = await fetch('https://example.com', {
dispatcher: proxy
});
console.log(res.status);
For hard rotation, create the dispatcher for that task and close it when the task ends. Connection pooling is useful until it hides rotation.
Axios must not fight your agent
When using custom HTTP or SOCKS agents with axios, disable axios built-in proxy parsing. Otherwise two proxy systems may try to own the same request.
import axios from 'axios';
import { SocksProxyAgent } from 'socks-proxy-agent';
const agent = new SocksProxyAgent(
'socks5h://USER:PASS@proxynade.net:2555'
);
const res = await axios.get('https://example.com', {
httpAgent: agent,
httpsAgent: agent,
proxy: false,
timeout: 30000,
});
Rotation strategy by job type
| Job | Rotation choice | Reason |
|---|---|---|
| Search result collection | Rotate per query batch | You want spread, not a new IP every asset. |
| Account login flow | Sticky session | A sudden exit change looks like a new machine. |
| Price monitoring | Rotate per domain or region | Keeps retries readable and cheap. |
| Blocked page retry | Change proxy and slow down | Retrying fast on one tunnel burns bandwidth. |
Log proxy label, target host, status, bytes, and retry count together. Otherwise a failure later looks like random provider quality.
Retry loops are where bandwidth goes
Most Node scrapers waste bandwidth after the first failure. A timeout triggers a retry. The retry reloads the same heavy page. The target returns the same block. The app records zero useful rows. The proxy still counts all of it.
Use small retry budgets, block waste assets in browser jobs, and stop retrying when the response class is obviously a target block.
Use a rotation lease
A clean Node rotation setup treats each proxy assignment as a lease. The lease owns the client agent, retry budget, and labels. When the lease ends, close the agent and stop using that exit.
async function withProxyLease(proxyUrl, task) {
const agent = new ProxyAgent(proxyUrl);
try {
return await task(agent);
} finally {
await agent.close();
}
}
await withProxyLease('http://USER:PASS@proxynade.net:2555', agent => {
return fetch('https://example.com', { dispatcher: agent });
});
This shape prevents a common fake rotation: one global agent, many proxy strings, and no proof that the connection changed.
Retry policy that does not burn the account
| Response | Retry? | Reason |
|---|---|---|
407 | No | Auth failed. Repeating burns time. |
403 | Maybe once with a new lease | Target blocked the route. |
| Timeout | Maybe once | Could be route or target load. |
429 | Slow down | Fast retries make the signal worse. |
Put this policy near the agent code. If each caller invents retries, bandwidth waste returns from another file.
Node proxy rotation FAQ
Does Node fetch use HTTP_PROXY automatically? Do not assume it. Use an explicit Undici dispatcher or an environment proxy agent you have tested.
Why does my proxy not rotate? The client may reuse the existing connection. Build rotation around task boundaries, not string assignment.
Should I use axios or fetch? Use whichever your code can instrument. The important part is clear agent ownership and retry logging.
What should I log? Proxy label, target host, status, duration, retry count, and billed GB window.