Using SOCKS5 Proxies with axios
The proxy option cannot carry SOCKS5, and leaving it set alongside an agent breaks both. What to install, what to disable, and how to prove it worked.
axios has a proxy option, and it does not do what a SOCKS5 user needs. Worse, it fails quietly: set both proxy and a SOCKS agent and you get behavior that depends on the URL scheme, the axios version, and which of the two happens to win. This guide covers the agent that actually carries SOCKS5, the single option you must turn off for it to work, and the pooling decisions that separate a script that runs for ten minutes from one that runs all night.
Why the proxy option is not the answer
The proxy object in axios configures an HTTP proxy, and only that. Under the hood axios hands it to Node's HTTP layer, which knows how to open a CONNECT tunnel and nothing else. SOCKS5 is negotiated in binary before any HTTP exists, so there is no field in that object that could express it. Passing a SOCKS host there produces a client that either ignores the setting or attempts an HTTP conversation with something that is not listening for one.
The mechanism that does work is an agent. Node's http.Agent is responsible for producing the socket a request runs over, and a SOCKS agent is one that performs the handshake first and hands back the resulting tunnel. Because it operates below HTTP, axios does not need to understand SOCKS at all; it only needs to be told to use the socket the agent produced.
npm install socks-proxy-agentimport axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://USERNAME:PASSWORD@HOST:PORT");
const client = axios.create({
httpAgent: agent,
httpsAgent: agent,
proxy: false, // required, see below
timeout: 45000,
});
const { data } = await client.get("https://api.ipify.org?format=json");
console.log(data);proxy: false is not optional
This is the line people leave out, and the symptom is confusing enough to be worth stating on its own.
axios reads proxy configuration from the environment as well as from your config object. If HTTP_PROXY, HTTPS_PROXY or ALL_PROXY is set anywhere in the process environment, and on a lot of machines one of them is, axios will apply it in addition to your agent. What follows is a request that either goes through the wrong proxy, or attempts to reach your SOCKS endpoint using HTTP semantics, or works on your laptop and fails on the build server because only one of them had the variable set.
proxy: false tells axios to stop making its own arrangements and use the agent it was given. Set it whenever you set an agent, even when you are confident the environment is clean, because the whole point is that you cannot see the environment your code will eventually run in. The wider family of environment-variable surprises across Node HTTP clients is catalogd in the Node.js proxy guide.
Both agent slots, every time
httpAgent handles http:// targets and httpsAgent handles https://. Setting only one is a common and slow-burning mistake: everything works until a redirect crosses schemes, at which point half your traffic silently leaves without the proxy.
That redirect case is worth dwelling on. axios follows redirects by default, and a redirect from https:// to http://, or the reverse, switches which agent applies. If only httpsAgent is set, the hop to http:// goes direct from your own address. You will not see an error; you will see a request that succeeded and an address that was never proxied. Set both slots to the same agent unless you have a specific reason to differ.
socks5h:// versus socks5://
The h decides who resolves the hostname. With socks5:// your machine resolves it and sends the proxy an address. With socks5h:// the name goes to the proxy and the proxy resolves it.
Prefer socks5h://. Local resolution means every destination you visit appears in your own resolver's logs even though the traffic itself is proxied, and it means names that resolve differently by region resolve for your region rather than the exit's. When you have paid for an exit in a particular country and then resolved its targets from your own desk, you are testing something other than what you think. The protocol-level explanation, with curl and Python examples, is in the SOCKS5 setup guide.
One agent, not one per request
An agent owns a connection pool. Creating one inside a request function throws that pool away on every call:
// Wrong: a new agent, a new pool, and a new SOCKS handshake per request.
async function fetchUrl(url) {
const agent = new SocksProxyAgent(PROXY_URL);
return axios.get(url, { httpAgent: agent, httpsAgent: agent, proxy: false });
}Every call there performs a fresh TCP connection and a fresh SOCKS negotiation. On a metered plan that is handshake overhead on every request; against a per-IP connection cap it is the quickest way to trip a limit your real concurrency is nowhere near.
Create the agent once, enable keep-alive, and cap the pool:
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://USERNAME:PASSWORD@HOST:PORT", {
keepAlive: true,
maxSockets: 20,
timeout: 30000,
});
export const client = axios.create({
httpAgent: agent,
httpsAgent: agent,
proxy: false,
timeout: 45000,
validateStatus: (s) => s < 500, // handle 4xx yourself rather than throwing
});maxSockets is the one to set deliberately. Node's default is effectively unbounded, so a Promise.all over a large array will try to open as many sockets as there are items. The resulting failures look exactly like the proxy refusing you, which sends people to change providers when the fix was one number in their own process.
Rotating across a pool
An agent per exit, chosen per request, is the straightforward shape:
const agents = PROXY_URLS.map(
(u) => new SocksProxyAgent(u, { keepAlive: true, maxSockets: 10 }),
);
let i = 0;
function nextAgent() {
return agents[i++ % agents.length];
}
export function get(url) {
const agent = nextAgent();
return axios.get(url, { httpAgent: agent, httpsAgent: agent, proxy: false });
}Round-robin suits independent requests. It suits logged-in work badly: an account that appears from a different address on consecutive calls is a far louder signal than any single address, and the usual result is a locked account rather than a blocked request. When a cookie or a session matters, hold one agent for the duration of that state. Sticky versus rotating sessions covers where the line falls.
Reading the errors
ECONNREFUSED on the proxy host and port means nothing accepted a TCP connection. The endpoint or port is wrong, or a firewall between you and it is dropping the attempt. This is before authentication, so credentials are not implicated.
SocksClientError with a message about authentication means the handshake reached the authentication stage and was rejected. SOCKS5 has no equivalent of HTTP's 407, so there is no status code to switch on. Check first that you are using the SOCKS port rather than the HTTP one: they are separate listeners, and the same credential is valid on both, so pointing at the wrong port produces an authentication-shaped failure that has nothing to do with the credential.
A socket that opens and then goes quiet is usually an exit that has been taken out from under you. Treat a timeout as a reason to try a different exit rather than to retry the same one immediately.
If the error is an HTTP 407 coming back from a target rather than from the negotiation, your request did not go where you expected. Fixing 407 Proxy Authentication Required walks through the ways credentials get mangled between your config and the wire.
Prove it before you trust it
Every configuration error described here produces a request that succeeds. That is what makes them expensive: the code looks like it is working. The only check worth anything is the address the destination reports back.
const { data } = await client.get("https://api.ipify.org?format=json");
console.log(data.ip); // must not be your own addressRun it once at startup in anything that matters, and fail loudly if the address returned belongs to you. A scraper that has silently been running from the office address for a week is a worse outcome than one that refused to start.
Node4 proxies accept SOCKS5 on a separate port from HTTP, using the same username and password. The connection string builder will produce the exact URL for a given proxy, and what each product includes lists the ports alongside the plans.