Using SOCKS5 Proxies with axios
axios's proxy option cannot carry SOCKS5. Install socks-proxy-agent, set it on both agent slots with proxy false, and check the address that comes back.
axios's proxy option configures an HTTP proxy only. SOCKS5 goes in through an agent, which performs the handshake below HTTP and hands axios a finished tunnel.
npm install axios socks-proxy-agentimport axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://USERNAME:PASSWORD@HOST:PORT", {
keepAlive: true,
maxSockets: 20,
});
export 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.ip); // the proxy's address, not yoursUse the SOCKS5 port; it is a separate listener from the HTTP port, on the same credential. On the dashboard's Proxies page, switch the Connect card's protocol to SOCKS5 and it shows the right port (the residential card lists a separate SOCKS5 Port field instead). Write the URL with the socks5h:// scheme, for the reason below. SOCKS5 is available on every paid product on the pricing page; the free tier is HTTP only.
The two settings people leave out
proxy: false. axios reads proxy configuration from HTTP_PROXY, HTTPS_PROXY and ALL_PROXY as well as from your config. If one of those is set in the process environment, axios applies it in addition to your agent, and the request either goes through the wrong proxy or tries to speak HTTP to your SOCKS5 endpoint. proxy: false tells axios to use the agent it was given and nothing else. Set it whenever you set an agent, because you cannot see the environment your code will eventually run in. The same variable behaves differently across Node clients; the Node.js proxy guide has the table.
Both agent slots. httpAgent handles http:// targets and httpsAgent handles https://. axios follows redirects by default, and a redirect that crosses schemes switches which agent applies. With only httpsAgent set, the hop to http:// leaves from your own address with no error. Set both slots to the same agent unless you have a 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 is resolved there. Prefer socks5h://: local resolution logs every destination on your own resolver, and resolves region-balanced names for your location rather than the exit's, so an exit bought in one country can be handed a server chosen for another. The SOCKS5 setup guide explains the protocol side with curl and Python examples.
One agent, not one per request
An agent owns a connection pool. Creating one inside a request function discards that pool 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 pays a fresh TCP connection and a fresh SOCKS negotiation, which adds handshake overhead on a metered plan and is the quickest route to a per-address connection cap. Create the agent once at module scope, as in the first example, with keepAlive: true.
maxSockets is the number to set deliberately. Node's default is effectively unbounded, so a Promise.all over a large array opens as many sockets as there are items, and the failures that follow look like the proxy refusing you.
Rotating across a pool
One agent per exit, chosen per request:
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 });
}That pattern is for a list of static proxies. A rotating or residential gateway is one URL, so one agent is enough, and the exit is chosen per SOCKS5 connection: with keepAlive: true a reused socket keeps its exit. For a fresh exit on every request, create that agent with keepAlive: false; to hold one exit across many connections, add a session id to the username.
Round-robin suits independent requests. For logged-in work, hold one agent for the life of the session, because an account that appears from a different address on consecutive calls is a stronger signal than any single address. Sticky versus rotating sessions covers where the line falls.
Reading the errors
ECONNREFUSEDon the proxy host and port: nothing accepted a TCP connection. Check the endpoint and port; credentials are not involved yet.SocksClientError: the SOCKS5 handshake failed. SOCKS5 has no 407, so check first that you are on the SOCKS5 port rather than the HTTP port (an HTTP listener answers with bytes the agent cannot parse, which is easy to misread as a rejected login), then the credential, and on a free account remember that SOCKS5 needs a paid plan.- A socket that opens and then goes quiet: usually an exit that has gone away. Try a different exit rather than retrying the same one.
- An HTTP
407from a target: the request did not go where you expected. Fixing 407 Proxy Authentication Required walks through the ways credentials get mangled between config and wire.
Confirm it at startup
Each of the mistakes above produces a request that succeeds, so the check that matters is the address the destination reports:
const { data } = await client.get("https://api.ipify.org?format=json");
if (data.ip === MY_OWN_ADDRESS) throw new Error("traffic is not leaving through the proxy");Run it once when the process starts, so a missing agent slot or a stray environment variable is caught before the first real request.