axios is the most popular HTTP client in Node, and its proxy support has the most notorious gotcha of any client we help people with: the built-in proxy option works for plain HTTP and quietly fails for HTTPS, which is nearly every real target. This guide covers proxies with axios the way that actually holds up in production: why the proxy option breaks on HTTPS, the httpsAgent fix, SOCKS5, authentication, rotating a pool, and the timeout and error handling an unattended scraper needs.
We run a proxy network and a live proxy checker, so the axios proxy report we see most is "it works on http sites and does nothing on https ones." That is not a bug in your code, it is the proxy option doing what it does, and the fix is to stop using it for HTTPS. Every example runs on axios 1.x. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns recently checked endpoints with no key.
The short version
Do not use the axios proxy option for HTTPS. Install https-proxy-agent, pass it as httpsAgent, and set proxy: false so axios does not fight the agent. That one pattern works for HTTP and HTTPS, handles authentication, and is the base for rotation.
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";
const agent = new HttpsProxyAgent("http://user:pass@203.0.113.7:8080");
const res = await axios.get("https://httpbin.org/ip", {
httpsAgent: agent,
proxy: false, // stop axios's own proxy handling
timeout: 15000,
});
console.log(res.data);
Why the proxy option fails on HTTPS
The built-in option looks right and works in the first test, because the first test is usually an http:// URL:
// Works for HTTP targets, unreliable for HTTPS
const res = await axios.get("http://httpbin.org/ip", {
proxy: { host: "203.0.113.7", port: 8080, auth: { username: "user", password: "pass" } },
});
The moment you point it at an https:// URL, axios handles the TLS and the proxy tunnel itself, and for a plain HTTP proxy carrying HTTPS through a CONNECT tunnel it commonly sends the request to the proxy as if it were the origin, or ignores the proxy and goes direct. The symptom is a request that either errors oddly or returns your real IP. Because almost every site you scrape is HTTPS, the practical rule is to treat the proxy option as HTTP-only and reach for an agent everywhere else.
axios proxy option
fine for http, breaks on https
https-proxy-agent
handles the CONNECT tunnel
httpsAgent + proxy:false
axios steps aside
Target over HTTPS
sees the proxy IP
Authentication
With the agent approach, credentials live in the proxy URL as user:pass@host:port, and the agent answers the proxy's challenge for you:
const agent = new HttpsProxyAgent("http://user:pass@203.0.113.7:8080");
await axios.get("https://example.com", { httpsAgent: agent, proxy: false });
If the password contains characters that are special in a URL, percent-encode it first with encodeURIComponent, or the URL parses wrong and you get a confusing failure that looks like a dead proxy. A wrong or missing password comes back as HTTP 407, which at least confirms the proxy itself is alive.
SOCKS5 with socks-proxy-agent
axios has no SOCKS support of its own. Install socks-proxy-agent and pass its agent as both httpAgent and httpsAgent so HTTP and HTTPS both route through it:
import axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5://user:pass@203.0.113.7:1080");
const res = await axios.get("https://httpbin.org/ip", {
httpAgent: agent,
httpsAgent: agent,
proxy: false,
timeout: 15000,
});
Use socks5h:// (or rely on the agent resolving remotely) to have the proxy perform DNS rather than your machine, which keeps your resolver from seeing every target. We cover that split in what is a SOCKS5 proxy.
Rotating a pool
Because the agent carries the proxy, rotation is choosing which agent to attach and retrying on a different exit when one fails:
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";
const POOL = [
"http://user:pass@203.0.113.7:8080",
"http://user:pass@203.0.113.24:3128",
"http://user:pass@198.51.100.14:8080",
];
async function getRotating(url, tries = 4) {
for (let i = 0; i < tries; i++) {
const proxy = POOL[Math.floor(Math.random() * POOL.length)];
try {
const res = await axios.get(url, {
httpsAgent: new HttpsProxyAgent(proxy),
proxy: false,
timeout: 15000,
});
return res.data;
} catch (err) {
// dead or blocked exit, try the next one
}
}
throw new Error(`all ${tries} proxies failed for ${url}`);
}
The retry has to pick a different exit, because retrying the same dead proxy just wastes the attempt. You do not have to keep the list current by hand; our free proxy API returns a fresh pool you can fetch at startup and split into POOL.
Timeouts and error handling
An unattended axios run needs a timeout, because without one a proxy that accepts the connection and stalls will hang the request on Node's default socket behaviour far longer than you want. Set timeout in milliseconds on every call, and catch the errors proxies actually raise:
try {
const res = await axios.get(url, { httpsAgent: agent, proxy: false, timeout: 15000 });
// use res.data
} catch (err) {
if (err.code === "ECONNREFUSED") {
// nothing listening: dead proxy or wrong port
} else if (err.code === "ECONNABORTED") {
// the timeout tripped: proxy connected then stalled
} else if (err.response && err.response.status === 407) {
// proxy alive, credentials wrong
} else {
// ETIMEDOUT, socket hang up, TLS errors, and so on
}
}
ECONNREFUSED and ECONNABORTED are the two you will see most: the first is a dead exit, the second is your timeout doing its job on a slow one. Both are cues to rotate to the next proxy rather than retry the same one.
Verify the exit IP
Confirm the proxy took effect instead of trusting that it did:
const real = (await axios.get("https://httpbin.org/ip", { timeout: 10000 })).data.origin;
const viaProxy = (await axios.get("https://httpbin.org/ip", {
httpsAgent: new HttpsProxyAgent(POOL[0]),
proxy: false,
timeout: 15000,
})).data.origin;
console.log("real:", real, "proxy:", viaProxy);
if (real === viaProxy) throw new Error("proxy is not changing your IP");
If the two match on an HTTPS request, you are almost certainly still using the proxy option somewhere instead of httpsAgent.
Where to go from here
axios is a great client with one sharp edge, and once the httpsAgent pattern is muscle memory the proxy part is boring, which is what you want. The rest is the discipline every scraper needs: verify each exit, bound concurrency, and rotate a pool that is alive.
For verifying exits, our proxy checker reports exit IP, anonymity grade, geolocation and latency in one paste. For the pool, the free proxy API hands you recently checked endpoints with no key. The Node.js proxy guide covers the native fetch and http paths if part of your code is not on axios, proxies for web scraping covers choosing the right proxy type, and how to avoid IP bans while scraping is the prevention checklist for when rotation is not enough. When a hand-rolled pool becomes a chore, a rotating gateway that returns a fresh residential IP per request at $0.44/GB lets you drop the agent-swapping and keep one endpoint.