Skip to content

Use a proxy in Node.js with undici, fetch and axios

Create an undici ProxyAgent for the netdash gateway and pass it as the dispatcher to undici's fetch. Node's built-in fetch ignores HTTPS_PROXY unless you opt in.

Updated
Reading time
6 min
Checked against
7 sources

This Node.js proxy guide uses undici, the HTTP client behind Node's own fetch, installed as a package so you can give each request a proxy. netdash is a standard HTTP proxy, so there is no SDK: a ProxyAgent pointed at the gateway with your username and password is all it takes. The examples are ES modules with top-level await: save them as .mjs files, or set "type": "module" in package.json. They were checked with undici 7, which needs Node.js 20.18.1 or later. The current major version, undici 8, needs Node.js 22.19 or later, so on Node.js 20 install undici@7.

Quick start: undici ProxyAgent with fetch

Install undici with npm install undici, replace USERNAME and PASSWORD with the credentials from the dashboard (Residential, then Generate), and run the file:

Checked against a local test proxy
import { ProxyAgent, fetch } from "undici";

const dispatcher = new ProxyAgent({
  uri: "http://scale-us.netdash.io:2000",
  token: "Basic " + Buffer.from("USERNAME-cc-US:PASSWORD").toString("base64"),
});

const res = await fetch("https://api.ipify.org?format=json", { dispatcher, signal: AbortSignal.timeout(30_000) });
console.log(await res.text());
Node.js

Example output

{"ip":"203.0.113.10"}

Your output shows a different address in the country you chose.

It prints the exit IP the website saw, for example {"ip":"203.0.113.10"}. The proxy URI starts with http:// even though the website is HTTPS: netdash is an HTTP proxy, and HTTPS requests go through a CONNECT tunnel, so TLS still runs end to end between Node and the website.

Import fetch from undici together with ProxyAgent, so the dispatcher and fetch come from the same package version. Node's built-in fetch is a separate copy of undici whose version depends on your Node.js release.

Why built-in fetch ignores HTTPS_PROXY in a Node.js proxy setup

Node's built-in fetch doesn't read HTTP_PROXY or HTTPS_PROXY by default. Setting the variables changes nothing until you opt in with NODE_USE_ENV_PROXY=1 or the --use-env-proxy flag. fetch honors the opt-in from Node.js 22.21.0 or 24.0.0; the http and https modules, and the flag, need Node.js 22.21.0 or 24.5.0. Once enabled, requests use the proxy unless you pass your own agent or the host matches NO_PROXY:

Syntax-checked
Shell
NODE_USE_ENV_PROXY=1 \
HTTPS_PROXY="http://USERNAME-cc-US:PASSWORD@scale-us.netdash.io:2000" \
NO_PROXY="localhost,127.0.0.1" \
node app.mjs

That switch applies to every request the process makes, including calls to model APIs, package registries and your own services. Those requests would go through netdash and count against your balance. An explicit dispatcher on the requests that fetch websites is easier to reason about, which is why the rest of this guide uses one.

Authenticate the proxy

undici offers two documented ways to send your credentials. The token option takes a complete Proxy-Authorization value, such as Basic plus the base64 of username:password, and takes precedence over anything else. Alternatively, put the credentials in the proxy URI: undici URL-decodes them and sends them as Basic authentication. The older auth option is deprecated.

Checked against a local test proxy
uri-credentials.mjs
import { ProxyAgent, fetch } from "undici";

const { NETDASH_USERNAME, NETDASH_PASSWORD } = process.env;
const user = encodeURIComponent(`${NETDASH_USERNAME}-cc-US`);
const pass = encodeURIComponent(NETDASH_PASSWORD);

const dispatcher = new ProxyAgent(`http://${user}:${pass}@scale-us.netdash.io:2000`);
const res = await fetch("https://api.ipify.org?format=json", {
  dispatcher,
  signal: AbortSignal.timeout(30_000),
});
console.log(await res.json());
await dispatcher.close();

Credentials

Keep NETDASH_USERNAME and NETDASH_PASSWORD in a .env file or your secret manager, never in source control or in a chat with an assistant. Don't log the proxy URI or the token: both contain your password.

Sticky sessions and exit countries

The username carries the options: -cc-DE sets the exit country, and -session-{ID}-ttl-{MINUTES} keeps one exit IP for the lifetime you set while that IP stays available. Reusing a session ID reuses the session; a new ID starts a new one. Session IDs use letters and digits only, and randomBytes(4).toString("hex") gives 8 of them, the same length the dashboard generates. Use GB for the United Kingdom, not UK, and the -eu gateway for exits in Europe.

Put the helper in its own module:

Checked against a local test proxy
netdash.mjs
import { randomBytes } from "node:crypto";
import { ProxyAgent } from "undici";

const { NETDASH_USERNAME, NETDASH_PASSWORD } = process.env;

export function netdashAgent({ country = "US", sessionId, ttl = 30,
                               gateway = "http://scale-us.netdash.io:2000" } = {}) {
  const cc = country.toUpperCase() === "UK" ? "GB" : country.toUpperCase();
  let username = `${NETDASH_USERNAME}-cc-${cc}`;
  if (sessionId) username += `-session-${sessionId}-ttl-${ttl}`;
  return new ProxyAgent({
    uri: gateway,
    token: `Basic ${Buffer.from(`${username}:${NETDASH_PASSWORD}`).toString("base64")}`,
  });
}

export const newSessionId = () => randomBytes(4).toString("hex");

Then create one agent per sticky session, use it for every request of the task, and close it when the task ends:

Checked against a local test proxy
sticky.mjs
import { fetch } from "undici";
import { netdashAgent, newSessionId } from "./netdash.mjs";

// One agent per sticky session: German exit through the Europe gateway
const agent = netdashAgent({ country: "DE", sessionId: newSessionId(),
                             gateway: "http://scale-eu.netdash.io:2000" });
for (let i = 0; i < 2; i++) {
  const res = await fetch("https://api.ipify.org?format=json", {
    dispatcher: agent,
    signal: AbortSignal.timeout(30_000),
  });
  console.log(await res.json()); // the same exit IP both times
}
await agent.close();

The dashboard's sticky presets are 15 minutes, 30 minutes, 12 hours and 7 days; rotating and sticky sessions explains how to choose. For rotating sessions, leave out sessionId and the gateway assigns exit IPs for you.

axios with https-proxy-agent

axios has its own proxy option and also reads http_proxy and https_proxy from the environment. For HTTPS websites, give axios an httpsAgent from https-proxy-agent, which opens a CONNECT tunnel and sends the credentials from the proxy URL as Proxy-Authorization, and set proxy: false so axios ignores its own proxy settings and the environment variables. Install both with npm install axios https-proxy-agent.

Checked against a local test proxy
axios-example.mjs
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";

const { NETDASH_USERNAME, NETDASH_PASSWORD } = process.env;
const proxy = `http://${NETDASH_USERNAME}-cc-US:${NETDASH_PASSWORD}@scale-us.netdash.io:2000`;

const res = await axios.get("https://api.ipify.org?format=json", {
  httpsAgent: new HttpsProxyAgent(proxy),
  proxy: false, // no built-in proxy handling, no proxy environment variables
  timeout: 30_000,
});
console.log(res.data);

For plain http:// websites, pass httpAgent: new HttpProxyAgent(proxy) from the http-proxy-agent package as well. Recent https-proxy-agent releases are ES modules only, which suits the .mjs files used here.

Concurrency without overloading a site

netdash sets no per-account concurrency limit, so the limit that matters is the one each website can handle. A small worker pool over one agent keeps the number of requests in flight fixed, and it stops as soon as the site pushes back with 403 or 429:

Checked against a local test proxy
pool.mjs
import { fetch } from "undici";
import { netdashAgent } from "./netdash.mjs";

const urls = Array.from({ length: 10 }, (_, i) => `https://example.com/catalog?page=${i + 1}`);
const queue = [...urls];
const results = [];
const agent = netdashAgent({ country: "US" });
let stopped = false;

async function worker() {
  while (queue.length > 0 && !stopped) {
    const url = queue.shift();
    const res = await fetch(url, { dispatcher: agent, signal: AbortSignal.timeout(30_000) });
    const body = await res.text();
    if (res.status === 403 || res.status === 429) {
      stopped = true; // the site is pushing back: stop, then check its terms and limits
      console.log(`${res.status} from ${url}, stopping`);
      return;
    }
    results.push({ url, status: res.status, bytes: body.length });
  }
}

await Promise.all(Array.from({ length: 5 }, worker)); // 5 requests in flight at most
console.table(results);
await agent.close();

Collect public data only, respect robots.txt and each site's terms, keep request rates reasonable and back off when a site slows you down. Don't collect personal data without a lawful basis, and don't access content behind logins or paywalls without authorization. netdash does not solve CAPTCHAs: if a site rate-limits or challenges your requests, slow down, check its terms, or use its official API. Read how we expect netdash to be used.

Errors and fixes

When undici's fetch fails, the message is usually just TypeError: fetch failed. Log err.cause, and the cause of that error in turn, to see what went wrong.

SymptomCauseFix
Requests leave from your own IPBuilt-in fetch ignores HTTPS_PROXY by defaultPass a dispatcher, or opt in with NODE_USE_ENV_PROXY=1 knowing it affects the whole process
TypeError: fetch failed with the cause Request was cancelled., whose own cause is Proxy response (407) !== 200 when HTTP Tunneling (undici 7)The gateway rejected the credentials with 407 Proxy Authentication RequiredCheck the password and the username parameters (-cc-, -session-...-ttl-...)
TypeError: fetch failed with the cause ECONNREFUSED or UND_ERR_CONNECT_TIMEOUTWrong host and port pair, or your network blocks the outbound portUse the host and port from the endpoint table; allow outbound TCP to the gateway port
Errors after passing a dispatcher to the built-in fetchNode's bundled fetch and the undici package can be different versions with different dispatcher interfacesImport fetch and ProxyAgent from the same undici package
HTTPS through axios fails or goes directaxios' built-in proxy option or the environment variables are in useUse httpsAgent with proxy: false
SyntaxError: Cannot use import statement outside a moduleThe file is loaded as CommonJS (an older Node.js release, or "type": "commonjs" in package.json)Use .mjs or set "type": "module"
The request never finishesNo timeout setPass signal: AbortSignal.timeout(30_000) (axios: timeout)

netdash forwards your traffic unchanged. It doesn't solve CAPTCHAs, render JavaScript or retry requests. Targeting is country-level.

Next steps

Driving a browser instead of an HTTP client? The Playwright proxy setup covers per-browser and per-context proxies. For Python, see the Python requests proxy guide, and for data collection at scale, web data collection. All other tools are in the proxy integration guides. New to netdash? Start with the quickstart and keep the proxy format reference at hand.

  • Puppeteer proxy setup

    An authenticated proxy per browser or per context, with the exit country in the username.

  • Troubleshooting

    407 errors, timeouts, requests that skip the proxy and the wrong exit country.

  • Browser automation

    End-to-end tests and permitted browser workflows from the countries your users are in.

Stuck? See troubleshooting or email support with your username (without the password) and the error message.

Email support

Sources

Third-party behaviour described on this page was checked against these documents.

  1. 01undici docs: ProxyAgent (uri, token, deprecated auth, credentials in the URI, close)Accessed 2026-10-09
  2. 02undici docs: Migrating from v7 to v8 (Node.js requirement, built-in fetch compatibility)Accessed 2026-10-09
  3. 03Node.js Learn: Enterprise network configuration (NODE_USE_ENV_PROXY, --use-env-proxy, versions)Accessed 2026-10-09
  4. 04axios docs: Request config (proxy, proxy: false, httpAgent, httpsAgent, timeout)Accessed 2026-10-09
  5. 05proxy-agents: https-proxy-agent README and source (CONNECT, Proxy-Authorization from the URL)Accessed 2026-10-09
  6. 06npm registry: undici package metadata (engines)Accessed 2026-10-09
  7. 07npm registry: https-proxy-agent package metadata (ES module only)Accessed 2026-10-09

Spotted something out of date? Tell us.

Privacy choices

We use cookies and similar technologies that this site needs to work and to remember your choice. We also use Google Analytics to understand which pages and features people use: in the EEA, the UK and Switzerland only with your consent. Live chat loads only if you open it. You can change your choice at any time under “Privacy choices” in the footer. Privacy Policy

Privacy choices

Choose what netdash.io may use. You can change this at any time under “Privacy choices” in the footer.

Necessary (always on)

Keeps the site working and secure, and remembers your privacy choices (nd_consent, 12 months).

In the EEA, the UK and Switzerland it stays off until you switch it on; elsewhere it is on unless you switch it off here. Google Analytics 4 measures page views, clicks on links and buttons, use of the price calculator, connection builder and code examples, scroll depth, file downloads, and page-performance metrics. It sets the cookies _ga and _ga_96MPNM6GHH, which last up to 12 months. Google processes this data, which may involve transfer to the United States. We do not send your name, email address or account details to Google Analytics. With this switched on we also remember a referral code or campaign you arrived with (nd_attr, 30 days), so a partner can be credited if you sign up.

Support chat (loads only when you open it)

Our live chat is provided by LiveChat. It loads only when you open the chat window. LiveChat then sets its own cookies and processes the messages you send and technical data about your browser.