Axios is the most popular HTTP client in the Node.js ecosystem, but its proxy support has quirks that aren't obvious from the docs. The built-in proxy config works for HTTP targets, breaks silently for HTTPS targets in certain configurations, and doesn't support proxy authentication the way you'd expect. Here's how to set it up correctly.
Basic Proxy Configuration
The built-in proxy option (HTTP targets only)
const axios = require('axios');
const response = await axios.get('http://httpbin.org/ip', {
proxy: {
host: 'gate.proxylabs.app',
port: 8080,
auth: {
username: 'your-username',
password: 'your-password',
},
},
});
console.log(response.data);
// { origin: '74.125.xxx.xxx' }
This works for HTTP targets. For HTTPS targets, it depends on your Axios version and Node.js version. Axios v1.x handles HTTPS proxy tunneling better than v0.x, but there are still edge cases where it fails silently (the request goes direct, bypassing the proxy entirely).
The reliable approach: https-proxy-agent
For production use, bypass Axios's built-in proxy handling and use an HTTP agent:
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const proxyUrl = 'http://your-username:[email protected]:8080';
const agent = new HttpsProxyAgent(proxyUrl);
const response = await axios.get('https://httpbin.org/ip', {
httpsAgent: agent,
proxy: false, // Disable built-in proxy — let the agent handle it
});
console.log(response.data);
Critical: set proxy: false when using a custom agent. If you don't, Axios may try to apply both the built-in proxy and the agent, resulting in double-proxying or connection errors.
Why proxy: false Matters
This is the most common Axios proxy bug. Here's what happens:
- Axios sees the
proxyconfig and rewrites the request URL to go through the proxy - The
httpsAgentalso routes through the proxy - The request goes through the proxy twice, or the proxy gets a malformed request
// WRONG — double proxy
const response = await axios.get('https://example.com', {
httpsAgent: new HttpsProxyAgent(proxyUrl),
proxy: {
host: 'gate.proxylabs.app',
port: 8080,
},
});
// CORRECT — agent handles everything
const response = await axios.get('https://example.com', {
httpsAgent: new HttpsProxyAgent(proxyUrl),
proxy: false,
});
Rotating Proxies with Axios
With a rotating proxy gateway like ProxyLabs, each new connection gets a different IP. Since https-proxy-agent creates a new connection per request by default, rotation happens automatically:
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const proxyUrl = 'http://your-username:[email protected]:8080';
async function fetchWithRotation(urls) {
const results = [];
for (const url of urls) {
// New agent per request = new proxy connection = new IP
const agent = new HttpsProxyAgent(proxyUrl);
try {
const response = await axios.get(url, {
httpsAgent: agent,
proxy: false,
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
},
});
results.push({ url, status: response.status, size: response.data.length });
} catch (error) {
results.push({ url, error: error.message });
}
}
return results;
}
Reusing agents vs. creating new ones
Creating a new HttpsProxyAgent per request has overhead (~2-5ms). For high-throughput scraping, you can reuse the agent — the proxy gateway still rotates IPs per connection:
// Shared agent — still rotates IPs because the gateway handles rotation
const sharedAgent = new HttpsProxyAgent(proxyUrl);
// All these requests go through different IPs
const promises = urls.map(url =>
axios.get(url, { httpsAgent: sharedAgent, proxy: false, timeout: 15000 })
);
const responses = await Promise.allSettled(promises);
Sticky Sessions (Same IP Across Requests)
For multi-step flows — login → navigate → scrape — you need the same IP for all requests. Add a session ID to the proxy username:
const { HttpsProxyAgent } = require('https-proxy-agent');
const { v4: uuidv4 } = require('uuid');
function createStickySession(country = null) {
const sessionId = uuidv4().slice(0, 8);
let username = `your-username-session-${sessionId}`;
if (country) {
username += `-country-${country}`;
}
const proxyUrl = `http://${username}:[email protected]:8080`;
const agent = new HttpsProxyAgent(proxyUrl);
const client = axios.create({
httpsAgent: agent,
proxy: false,
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept-Language': 'en-US,en;q=0.9',
},
});
return { client, sessionId };
}
// Usage — all requests through the same IP
const { client, sessionId } = createStickySession('US');
const page1 = await client.get('https://example.com/login');
const page2 = await client.post('https://example.com/api/auth', credentials);
const page3 = await client.get('https://example.com/dashboard');
For a deep dive into sticky sessions, see our sticky sessions guide.
Concurrent Requests with Proxy
Axios + Promise.allSettled for controlled concurrency:
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
async function scrapeWithConcurrency(urls, maxConcurrent = 10) {
const proxyUrl = 'http://your-username:[email protected]:8080';
const results = [];
// Process in batches
for (let i = 0; i < urls.length; i += maxConcurrent) {
const batch = urls.slice(i, i + maxConcurrent);
const promises = batch.map(url => {
const agent = new HttpsProxyAgent(proxyUrl);
return axios.get(url, {
httpsAgent: agent,
proxy: false,
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
}).then(resp => ({ url, status: resp.status, data: resp.data }))
.catch(err => ({ url, error: err.message }));
});
const batchResults = await Promise.allSettled(promises);
results.push(...batchResults.map(r => r.value || r.reason));
// Rate limiting between batches
if (i + maxConcurrent < urls.length) {
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
}
}
return results;
}
For more controlled concurrency, use p-limit:
const pLimit = require('p-limit');
const limit = pLimit(10); // 10 concurrent requests max
const proxyUrl = 'http://your-username:[email protected]:8080';
const results = await Promise.all(
urls.map(url =>
limit(async () => {
const agent = new HttpsProxyAgent(proxyUrl);
const resp = await axios.get(url, {
httpsAgent: agent,
proxy: false,
timeout: 15000,
});
return { url, status: resp.status };
})
)
);
Axios Interceptors for Proxy Retry Logic
Axios interceptors let you add retry logic at the request/response level:
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
function createProxyClient(proxyUsername, proxyPassword, maxRetries = 3) {
const client = axios.create({
timeout: 15000,
proxy: false,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
});
// Add proxy agent to every request
client.interceptors.request.use(config => {
const proxyUrl = `http://${proxyUsername}:${proxyPassword}@gate.proxylabs.app:8080`;
config.httpsAgent = new HttpsProxyAgent(proxyUrl);
return config;
});
// Retry on proxy errors
client.interceptors.response.use(
response => response,
async error => {
const config = error.config;
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount >= maxRetries) {
return Promise.reject(error);
}
const retryable =
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT' ||
error.code === 'ECONNREFUSED' ||
(error.response && error.response.status === 502) ||
(error.response && error.response.status === 429);
if (!retryable) {
return Promise.reject(error);
}
config.__retryCount += 1;
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, config.__retryCount) + Math.random() * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, delay));
// New agent = new IP on retry
const proxyUrl = `http://${proxyUsername}:${proxyPassword}@gate.proxylabs.app:8080`;
config.httpsAgent = new HttpsProxyAgent(proxyUrl);
return client(config);
}
);
return client;
}
// Usage
const client = createProxyClient('your-username', 'your-password');
const data = await client.get('https://api.example.com/products');
Geo-Targeted API Requests
For monitoring APIs that serve location-specific responses:
function createGeoClient(country, city = null) {
let username = `your-username-country-${country}`;
if (city) {
username += `-city-${city}`;
}
const proxyUrl = `http://${username}:[email protected]:8080`;
const agent = new HttpsProxyAgent(proxyUrl);
return axios.create({
httpsAgent: agent,
proxy: false,
timeout: 15000,
});
}
// Compare API responses from different locations
const usClient = createGeoClient('US', 'NewYork');
const ukClient = createGeoClient('GB', 'London');
const deClient = createGeoClient('DE', 'Berlin');
const [usResp, ukResp, deResp] = await Promise.all([
usClient.get('https://api.example.com/pricing'),
ukClient.get('https://api.example.com/pricing'),
deClient.get('https://api.example.com/pricing'),
]);
Environment Variables and Configuration
// config.js
const proxyConfig = {
host: process.env.PROXY_HOST || 'gate.proxylabs.app',
port: parseInt(process.env.PROXY_PORT || '8080'),
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD,
};
function getProxyUrl(options = {}) {
let username = proxyConfig.username;
if (options.session) username += `-session-${options.session}`;
if (options.country) username += `-country-${options.country}`;
if (options.city) username += `-city-${options.city}`;
return `http://${username}:${proxyConfig.password}@${proxyConfig.host}:${proxyConfig.port}`;
}
module.exports = { getProxyUrl };
Axios vs. Other Node.js HTTP Clients
| Feature | Axios | node-fetch | got | undici |
|---|---|---|---|---|
| Built-in proxy support | Partial (HTTP only reliably) | None | Via agent | Via dispatcher |
| Agent-based proxy | Yes (httpsAgent) | Yes (agent) | Yes (agent) | Yes (ProxyAgent) |
| Interceptors | Yes | No | Hooks | No |
| Retry built-in | No (use interceptors) | No | Yes (retry) | No |
| HTTP/2 support | No | No | Yes | Yes |
| Bundle size | 29KB | 0KB (built-in on 18+) | 48KB | Built-in on 18+ |
If you're starting a new project and don't need interceptors, got with its built-in retry and proxy agent support is worth considering. If you're already using Axios (most Node.js projects are), the patterns above will work reliably.
For browser automation with proxies in Node.js, see the Puppeteer proxy setup guide or Playwright integration. To verify your proxy is working correctly before deploying, use the proxy tester.
Ready to try the fastest residential proxies?
Join developers and businesses who trust ProxyLabs for mission-critical proxy infrastructure.
Building proxy infrastructure since 2019. Previously failed at many things, now failing slightly less.
Related Articles
Puppeteer Proxy Setup: Headless Chrome with Rotating IPs
Configure Puppeteer with rotating residential proxies. Covers proxy authentication, page-level rotation, session persistence, and stealth plugins.
6 min readHow to Set Up a Proxy with cURL (Every Option Explained)
Complete cURL proxy reference: HTTP, HTTPS, SOCKS5, authentication, geo-targeting, and troubleshooting. Every flag and option with working examples.
8 min readContinue exploring
Implementation guides for requests, Scrapy, Axios, Puppeteer, and more.
See how residential proxies fit large-scale scraping workflows.
Evaluate ProxyLabs against Bright Data, Oxylabs, Smartproxy, and others.
Browse location coverage and targeting options across 195+ countries.