Node.js 18+ ships fetch, but it doesn't take a proxy setting. The fix is a dispatcher (for undici) or an agent (for axios and other HTTP clients). This guide shows both, with authenticated crawlproxies credentials, then covers rotation, sticky sessions and running many requests at once.
Get your username and password from the generator. The examples use the Residential gateway geo.crawlproxies.com (HTTP port 8080, SOCKS5 port 1080).
Option 1: undici (recommended)
undici is the HTTP client that powers Node's own fetch. Install it and use its fetch together with a ProxyAgent:
npm i undiciimport { fetch, ProxyAgent } from "undici";
const agent = new ProxyAgent({
uri: "http://geo.crawlproxies.com:8080",
token: "Basic " + Buffer.from("USERNAME:PASSWORD").toString("base64"),
});
const res = await fetch("https://ipinfo.io/json", { dispatcher: agent });
console.log(res.status, await res.json());The token becomes the Proxy-Authorization header. HTTPS sites are tunnelled through the proxy, so the connection to the website stays encrypted.
Tip: importfetchfromundicirather than using the global one. The globalfetchuses the copy of undici bundled with your Node version, and mixing it with a different installed version can fail in confusing ways.
To send every undici request through the proxy without passing dispatcher each time:
import { fetch, ProxyAgent, setGlobalDispatcher } from "undici";
setGlobalDispatcher(new ProxyAgent({
uri: "http://geo.crawlproxies.com:8080",
token: "Basic " + Buffer.from("USERNAME:PASSWORD").toString("base64"),
}));
const res = await fetch("https://ipinfo.io/ip");Option 2: axios with a proxy agent
axios has a built-in proxy option, but for HTTPS sites an agent is more reliable. Install https-proxy-agent:
npm i axios https-proxy-agentimport axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";
const agent = new HttpsProxyAgent("http://USERNAME:PASSWORD@geo.crawlproxies.com:8080");
const { data } = await axios.get("https://ipinfo.io/json", {
httpsAgent: agent,
proxy: false, // turn off axios's own proxy handling so the agent is used
timeout: 30_000,
});
console.log(data.ip, data.country);For plain http:// sites, add http-proxy-agent and pass its HttpProxyAgent as httpAgent the same way.
Option 3: SOCKS5
npm i socks-proxy-agentimport axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://USERNAME:PASSWORD@geo.crawlproxies.com:1080");
const { data } = await axios.get("https://ipinfo.io/json", {
httpAgent: agent,
httpsAgent: agent,
proxy: false,
});socks5h resolves hostnames through the proxy as well, so DNS lookups don't come from your machine.
Geo-targeting
Location is part of the username, so it's a string change:
const username = (opts = {}) => {
let u = "USERNAME";
if (opts.country) u += `-country-${opts.country}`;
if (opts.country && opts.city) u += `-city-${opts.city}`;
return u;
};
const agent = new ProxyAgent({
uri: "http://geo.crawlproxies.com:8080",
token: "Basic " + Buffer.from(`${username({ country: "gb", city: "london" })}:PASSWORD`).toString("base64"),
});Country codes are two letters; cities and US states are lowercase with no spaces. The geo-targeting guide lists the options.
Rotation and sticky sessions
Without a session in the username, every new connection gets a new IP. Agents and dispatchers keep connections open and reuse them, so a burst of requests through one agent can share an exit IP. When you need a guaranteed fresh IP, use a fresh agent and close it when you're done:
const TOKEN = "Basic " + Buffer.from("USERNAME:PASSWORD").toString("base64");
async function freshIp() {
const agent = new ProxyAgent({ uri: "http://geo.crawlproxies.com:8080", token: TOKEN });
try {
const res = await fetch("https://ipinfo.io/ip", { dispatcher: agent });
return (await res.text()).trim();
} finally {
await agent.close();
}
}For the opposite (the same IP across a login or checkout), add a session id and a duration in seconds to the username:
import { randomUUID } from "node:crypto";
const sessionUser = `USERNAME-country-us-session-${randomUUID().slice(0, 8)}-time-1800`;Keep one session id per account or task. Sticky vs rotating sessions explains the trade-offs.
Many requests at once
Promise.all on hundreds of URLs opens hundreds of connections at once. A small pool keeps it under control:
async function mapLimit(items, limit, fn) {
const results = new Array(items.length);
let next = 0;
const worker = async () => {
while (next < items.length) {
const i = next++;
try { results[i] = await fn(items[i]); }
catch (err) { results[i] = { error: err.message }; }
}
};
await Promise.all(Array.from({ length: limit }, worker));
return results;
}
const urls = Array.from({ length: 50 }, (_, i) => `https://example.com/page/${i + 1}`);
const results = await mapLimit(urls, 10, async (url) => {
const res = await fetch(url, { dispatcher: agent, signal: AbortSignal.timeout(30_000) });
return { url, status: res.status };
});Start around 10 concurrent requests and raise it slowly while watching your error rate.
Errors you might see
| Error | Usually means |
|---|---|
Proxy response (407) !== 200 when HTTP Tunneling (undici) | Wrong username or password |
Request failed with status code 407 (axios) | The same: check the credentials and targeting syntax |
ECONNREFUSED | Wrong host or port, or HTTP port used with a SOCKS5 client |
TimeoutError / ETIMEDOUT | A slow exit IP: retry, and keep timeouts on every request |
403 / 429 from the site | Blocking or rate limiting: see the debugging checklist |
Driving a real browser instead? See Playwright and Puppeteer with authenticated proxies.



