How to Configure Puppeteer With a Residential Proxy
To use Puppeteer with a residential proxy, pass the --proxy-server flag when launching the browser and authenticate with page.authenticate() for user:pass proxies. Residential proxies route your traffic through real ISP-assigned IPs, making automated requests look like genuine household traffic — critical for avoiding detection when scraping, testing geo-targeted content, or managing multiple accounts. Below is the full setup with working Node.js code, rotation strategies, and error handling.
Why Residential Proxies Matter for Puppeteer Automation
Puppeteer scripts running without proxies hit websites from your server’s datacenter IP — an IP address that WHOIS databases, IP reputation services, and bot detection systems immediately recognize as non-residential. The consequences:
- CAPTCHAs on every request — services like Cloudflare, PerimeterX, and DataDome flag datacenter IPs with higher challenge rates.
- IP bans after minimal volume — many websites rate-limit or block datacenter IP ranges entirely.
- Geo-targeting failures — your datacenter IP may resolve to a different country than your target audience, skewing localized content and pricing.
Residential proxies solve all three problems. They route traffic through IPs assigned by real ISPs to real households, so the target website sees a request from a typical consumer connection. IP reputation is clean, geo-location is accurate, and detection rates drop dramatically.
Datacenter vs. Residential vs. Mobile Proxies
| Proxy Type | IP Source | Detection Risk | Speed | Cost | Best For |
|---|---|---|---|---|---|
| Datacenter | Cloud providers (AWS, GCP) | High | Very fast | Cheapest | Non-sensitive scraping, testing |
| Residential | Real ISP connections | Low | Moderate | Mid-range | Stealth scraping, account management, geo-testing |
| Mobile | Cellular carriers | Very low | Variable | Expensive | Social media automation, highest-trust tasks |
For most Puppeteer automation tasks — price monitoring, SERP scraping, ad verification, account management — residential proxies hit the sweet spot between stealth and cost. For a deeper look at how proxy configurations interact with browser isolation, see our guide on proxy browsers.
Prerequisites
Before starting, make sure you have:
- Node.js 18+ installed (
node -vto check) - Puppeteer installed:
npm install puppeteer - A residential proxy provider — you’ll need at minimum: host, port, username, and password. Most providers (Bright Data, Oxylabs, Smartproxy, IPRoyal, SOAX) provide credentials in
user:pass@host:portformat.
Automate Puppeteer With Residential Proxy With Send.win
Send.win pairs isolated, fingerprint-managed browser profiles with a full Automation API, so your scripts run in profiles that look and behave like real, separate users:
- Selenium, Puppeteer & Playwright support – drive any profile programmatically (Team plan)
- Isolated profiles – each with its own fingerprint, cookies, and storage
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Desktop app for Windows, macOS & Linux – plus cloud sessions when you don’t want a local install
Try the instant cloud browser demo — no install, straight from your browser. Then compare plans: a 30-day free trial with no credit card, and paid plans from $6.99/month billed annually.
Step 1: Basic Puppeteer Setup With a Single Residential Proxy
The simplest configuration — launch Puppeteer with a single proxy and authenticate:
const puppeteer = require('puppeteer');
async function runWithProxy() {
const PROXY_HOST = 'gate.smartproxy.com';
const PROXY_PORT = 10001;
const PROXY_USER = 'your_username';
const PROXY_PASS = 'your_password';
const browser = await puppeteer.launch({
headless: 'new',
args: [
`--proxy-server=http://${PROXY_HOST}:${PROXY_PORT}`,
'--no-sandbox',
'--disable-setuid-sandbox',
],
});
const page = await browser.newPage();
// Authenticate with the proxy
await page.authenticate({
username: PROXY_USER,
password: PROXY_PASS,
});
// Verify the proxy is working
await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
const ipText = await page.$eval('body', (el) => el.textContent);
console.log('Current IP:', ipText);
await browser.close();
}
runWithProxy().catch(console.error);
How it works:
--proxy-servertells Chromium to route all traffic through the specified proxy host and port.page.authenticate()handles HTTP proxy authentication (username/password). This fires before the first request on the page, so every resource load goes through the authenticated proxy.- The
httpbin.org/ipcheck confirms the proxy IP, not your server’s IP, is being used.
Step 2: Proxy Rotation Strategies
A single residential IP will eventually get flagged if you send too many requests. Rotation distributes requests across many IPs, keeping each one below the detection threshold.
Strategy A: Gateway Rotation (Provider-Side)
Most residential proxy providers offer a rotating gateway — a single endpoint that automatically assigns a different IP for each request or session. This is the simplest approach:
const puppeteer = require('puppeteer');
async function runWithRotatingGateway() {
// Rotating gateway — provider assigns a new IP per connection
const PROXY_HOST = 'gate.provider.com';
const PROXY_PORT = 10000;
const PROXY_USER = 'user-country-us';
const PROXY_PASS = 'password123';
const urls = [
'https://httpbin.org/ip',
'https://httpbin.org/headers',
'https://httpbin.org/user-agent',
];
for (const url of urls) {
// Each browser instance gets a different IP from the rotating gateway
const browser = await puppeteer.launch({
headless: 'new',
args: [`--proxy-server=http://${PROXY_HOST}:${PROXY_PORT}`],
});
const page = await browser.newPage();
await page.authenticate({ username: PROXY_USER, password: PROXY_PASS });
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
const content = await page.$eval('body', (el) => el.textContent);
console.log(`URL: ${url}`);
console.log(`Response: ${content.trim().substring(0, 200)}\n`);
await browser.close();
}
}
runWithRotatingGateway().catch(console.error);
Notice we launch a new browser instance per URL. With rotating gateways, a new connection typically means a new IP. Some providers also support sticky sessions (same IP for a set duration) by appending a session ID to the username — useful when you need to maintain a consistent identity across multiple pages on the same site.
Strategy B: Proxy Pool Rotation (Client-Side)
If you have a list of individual proxy endpoints (not a rotating gateway), rotate through them in your code:
const puppeteer = require('puppeteer');
const proxyPool = [
{ host: '192.168.1.100', port: 8080, user: 'user1', pass: 'pass1' },
{ host: '192.168.1.101', port: 8080, user: 'user2', pass: 'pass2' },
{ host: '192.168.1.102', port: 8080, user: 'user3', pass: 'pass3' },
{ host: '192.168.1.103', port: 8080, user: 'user4', pass: 'pass4' },
];
let currentIndex = 0;
function getNextProxy() {
const proxy = proxyPool[currentIndex];
currentIndex = (currentIndex + 1) % proxyPool.length;
return proxy;
}
async function scrapeWithRotation(urls) {
for (const url of urls) {
const proxy = getNextProxy();
console.log(`Using proxy: ${proxy.host}:${proxy.port}`);
const browser = await puppeteer.launch({
headless: 'new',
args: [`--proxy-server=http://${proxy.host}:${proxy.port}`],
});
const page = await browser.newPage();
await page.authenticate({ username: proxy.user, password: proxy.pass });
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
const title = await page.title();
console.log(`Title: ${title}\n`);
} catch (err) {
console.error(`Failed on ${url} with proxy ${proxy.host}: ${err.message}`);
} finally {
await browser.close();
}
}
}
const targetUrls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3',
'https://example.com/page4',
];
scrapeWithRotation(targetUrls).catch(console.error);
Step 3: Robust Error Handling for Proxy Failures
Residential proxies are inherently less reliable than datacenter proxies — they route through real consumer connections that can go offline. Your code must handle failures gracefully:
const puppeteer = require('puppeteer');
const proxyPool = [
{ host: '192.168.1.100', port: 8080, user: 'user1', pass: 'pass1' },
{ host: '192.168.1.101', port: 8080, user: 'user2', pass: 'pass2' },
{ host: '192.168.1.102', port: 8080, user: 'user3', pass: 'pass3' },
];
async function fetchWithRetry(url, maxRetries = 3) {
const failedProxies = new Set();
for (let attempt = 1; attempt <= maxRetries; attempt++) {
// Pick a proxy that hasn't failed yet
const availableProxies = proxyPool.filter(
(p) => !failedProxies.has(`${p.host}:${p.port}`)
);
if (availableProxies.length === 0) {
throw new Error('All proxies exhausted for this request');
}
const proxy = availableProxies[Math.floor(Math.random() * availableProxies.length)];
let browser;
try {
browser = await puppeteer.launch({
headless: 'new',
args: [
`--proxy-server=http://${proxy.host}:${proxy.port}`,
'--no-sandbox',
],
});
const page = await browser.newPage();
await page.authenticate({ username: proxy.user, password: proxy.pass });
// Set reasonable timeout for residential proxies (slower than datacenter)
page.setDefaultTimeout(45000);
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 45000,
});
if (!response || response.status() >= 400) {
throw new Error(`HTTP ${response ? response.status() : 'no response'}`);
}
const content = await page.content();
console.log(`✅ Success on attempt ${attempt} via ${proxy.host}`);
return content;
} catch (err) {
console.warn(
`⚠️ Attempt ${attempt}/${maxRetries} failed via ${proxy.host}: ${err.message}`
);
failedProxies.add(`${proxy.host}:${proxy.port}`);
} finally {
if (browser) await browser.close();
}
// Delay before retry to avoid hammering
if (attempt < maxRetries) {
const delay = 2000 * attempt;
console.log(`Waiting ${delay}ms before retry...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error(`Failed to fetch ${url} after ${maxRetries} attempts`);
}
// Usage
fetchWithRetry('https://example.com')
.then((html) => console.log(`Fetched ${html.length} chars`))
.catch((err) => console.error('Final failure:', err.message));
Key error-handling patterns:
- Retry with different proxies — when a proxy fails, mark it as failed and try a different one.
- Exponential backoff — increase the delay between retries to avoid triggering rate limits.
- Timeout tuning — residential proxies are slower than datacenter; set timeouts to 30-45 seconds rather than the default 30s.
- HTTP status checking — a 200 from the proxy doesn’t mean success; check the target’s response status too.
Step 4: Combining Residential Proxies With Antidetect Browsers
Residential proxies handle IP-level detection, but modern anti-bot systems also fingerprint your browser. Puppeteer’s default Chromium instance leaks automation signals: the navigator.webdriver property, missing browser plugins, Chrome DevTools protocol artifacts, and unrealistic screen dimensions.
This is where an antidetect browser like Send.win complements your proxy setup. Instead of launching Puppeteer’s bundled Chromium, you connect Puppeteer to a pre-configured Sendwin Browser profile that already has a realistic fingerprint — canvas hash, WebGL renderer, audio context, font list, screen dimensions — and a residential proxy baked in. For a deeper understanding of how browser fingerprinting exposes Selenium and Puppeteer scripts, read our guide on Selenium browser fingerprinting.
The workflow looks like this:
- Create a profile in Sendwin Browser with a unique fingerprint and assign a residential proxy to it.
- Start the profile with its local automation endpoint enabled (available on the Pro plan at $9.99/month or $6.99/month annual).
- Connect Puppeteer to the running profile via
puppeteer.connect()rather thanpuppeteer.launch().
const puppeteer = require('puppeteer');
async function connectToAntidetectProfile() {
// Connect to a running Sendwin Browser profile's automation endpoint
// The profile already has a residential proxy and unique fingerprint configured
const profileWSEndpoint = 'ws://127.0.0.1:9222/devtools/browser/PROFILE_ID';
const browser = await puppeteer.connect({
browserWSEndpoint: profileWSEndpoint,
defaultViewport: null, // Use the profile's configured viewport
});
const page = await browser.newPage();
// No need to set proxy or authenticate — the profile handles it
// No need for stealth plugins — the profile's fingerprint is already realistic
await page.goto('https://bot.sannysoft.com', {
waitUntil: 'networkidle2',
timeout: 60000,
});
// Take a screenshot to verify fingerprint passes
await page.screenshot({ path: 'fingerprint-test.png', fullPage: true });
console.log('Screenshot saved — check fingerprint test results');
// Disconnect (don't close — the profile stays running)
browser.disconnect();
}
connectToAntidetectProfile().catch(console.error);
The advantage is layered stealth: the residential proxy provides a clean IP, while the antidetect profile provides a clean fingerprint. Anti-bot systems like Cloudflare, DataDome, and PerimeterX check both layers — passing one but failing the other still gets you blocked. To understand the full spectrum of techniques websites use to detect bots, see our anti-bot bypass guide.
Step 5: Performance Optimization Tips
Residential proxies add latency. Here’s how to keep your Puppeteer scripts fast despite the proxy overhead:
Block Unnecessary Resources
const puppeteer = require('puppeteer');
async function optimizedScrape(url, proxyConfig) {
const browser = await puppeteer.launch({
headless: 'new',
args: [`--proxy-server=http://${proxyConfig.host}:${proxyConfig.port}`],
});
const page = await browser.newPage();
await page.authenticate({
username: proxyConfig.user,
password: proxyConfig.pass,
});
// Block images, fonts, and stylesheets to save bandwidth
await page.setRequestInterception(true);
page.on('request', (req) => {
const resourceType = req.resourceType();
if (['image', 'font', 'stylesheet', 'media'].includes(resourceType)) {
req.abort();
} else {
req.continue();
}
});
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
const data = await page.evaluate(() => document.title);
await browser.close();
return data;
}
Additional Performance Tips
- Use
domcontentloadedovernetworkidle0— the latter waits for all network activity to stop, which is slow on residential proxies with higher latency. Usedomcontentloadedunless you specifically need all async resources loaded. - Limit concurrent browser instances — residential proxies have bandwidth caps. Running 20 concurrent browsers eats bandwidth fast. Use a concurrency limiter (e.g.,
p-limit) to cap at 3-5 simultaneous instances. - Cache DNS locally — residential proxies often have slower DNS resolution. Using
--dns-prefetch-disableflag and pre-resolving hostnames can help. - Monitor bandwidth usage — residential proxy providers bill by GB. Blocking unnecessary resources (step above) can cut bandwidth consumption by 40-60%. If you’re using Send.win’s included proxy bandwidth (5GB on Pro, 20GB on Team), resource blocking extends your allocation significantly.
Common Pitfalls and How to Avoid Them
| Pitfall | What Happens | Solution |
|---|---|---|
Forgetting page.authenticate() |
Proxy returns 407 Proxy Authentication Required | Always call page.authenticate() before page.goto() |
Using http:// proxy with HTTPS sites |
SSL errors or connection refused | Chromium handles HTTPS over HTTP proxies via CONNECT tunneling — the --proxy-server=http:// flag is correct even for HTTPS targets |
| Setting timeout too low | Frequent timeouts on residential connections | Set 30-45 second timeouts; residential proxies are slower than datacenter |
Not checking navigator.webdriver |
Bot detection catches automation flags | Use stealth plugins or connect through an antidetect browser profile |
| Reusing one IP for too many requests | IP gets flagged and blocked | Rotate IPs — either via provider gateway or client-side pool |
| Ignoring WebGL fingerprint leaks | Same GPU fingerprint across all sessions | Use an antidetect browser that spoofs WebGL — see our WebGL fingerprinting guide |
Full Working Script: Scraper With Proxy Rotation and Error Handling
Here’s a complete, production-ready script combining everything above:
const puppeteer = require('puppeteer');
// Configuration
const CONFIG = {
maxRetries: 3,
timeout: 45000,
concurrency: 3,
delayBetweenRequests: 2000,
blockResources: ['image', 'font', 'stylesheet', 'media'],
};
const proxyPool = [
{ host: 'gate.smartproxy.com', port: 10001, user: 'user-country-us', pass: 'pass123' },
{ host: 'gate.smartproxy.com', port: 10002, user: 'user-country-uk', pass: 'pass123' },
{ host: 'gate.smartproxy.com', port: 10003, user: 'user-country-de', pass: 'pass123' },
];
let proxyIndex = 0;
function getNextProxy() {
const proxy = proxyPool[proxyIndex];
proxyIndex = (proxyIndex + 1) % proxyPool.length;
return proxy;
}
async function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function scrapePage(url) {
let lastError;
for (let attempt = 1; attempt <= CONFIG.maxRetries; attempt++) {
const proxy = getNextProxy();
let browser;
try {
browser = await puppeteer.launch({
headless: 'new',
args: [
`--proxy-server=http://${proxy.host}:${proxy.port}`,
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
],
});
const page = await browser.newPage();
await page.authenticate({ username: proxy.user, password: proxy.pass });
// Block heavy resources
await page.setRequestInterception(true);
page.on('request', (req) => {
if (CONFIG.blockResources.includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
// Set realistic viewport
await page.setViewport({ width: 1366, height: 768 });
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: CONFIG.timeout,
});
if (!response || response.status() >= 400) {
throw new Error(`HTTP ${response ? response.status() : 'null'}`);
}
const result = {
url,
status: response.status(),
title: await page.title(),
html: await page.content(),
proxy: `${proxy.host}:${proxy.port}`,
};
console.log(`✅ [${url}] → ${result.title} (via ${result.proxy})`);
return result;
} catch (err) {
lastError = err;
console.warn(`⚠️ Attempt ${attempt}/${CONFIG.maxRetries} for ${url}: ${err.message}`);
if (attempt < CONFIG.maxRetries) await delay(2000 * attempt);
} finally {
if (browser) await browser.close();
}
}
console.error(`❌ Failed: ${url} — ${lastError.message}`);
return { url, error: lastError.message };
}
async function scrapeAll(urls) {
const results = [];
// Process in batches to respect concurrency limit
for (let i = 0; i < urls.length; i += CONFIG.concurrency) {
const batch = urls.slice(i, i + CONFIG.concurrency);
const batchResults = await Promise.all(batch.map((url) => scrapePage(url)));
results.push(...batchResults);
// Delay between batches
if (i + CONFIG.concurrency < urls.length) {
await delay(CONFIG.delayBetweenRequests);
}
}
// Summary
const successes = results.filter((r) => !r.error).length;
const failures = results.filter((r) => r.error).length;
console.log(`\n📊 Results: ${successes} succeeded, ${failures} failed out of ${urls.length}`);
return results;
}
// Run
const targetUrls = [
'https://example.com',
'https://httpbin.org/ip',
'https://httpbin.org/headers',
'https://httpbin.org/user-agent',
];
scrapeAll(targetUrls).catch(console.error);
🏆 Send.win Verdict
Residential proxies handle the IP layer, but browser fingerprinting catches what proxies can’t hide. Send.win solves both problems: each Sendwin Browser profile pairs a unique, realistic browser fingerprint with your residential proxy, and the Automation API lets you connect Puppeteer directly to those profiles. Instead of fighting navigator.webdriver detection and canvas fingerprinting with fragile stealth plugins, you get a production-grade fingerprint out of the box. The Pro plan at $6.99/month (annual) includes 150 profiles and the Automation API — enough for serious scraping and multi-account automation setups.
Try Send.win free today — 30-day trial, no credit card, and connect Puppeteer to stealth-ready profiles in minutes.
Frequently Asked Questions
Does Puppeteer support SOCKS5 residential proxies?
Yes. Replace the --proxy-server value with socks5://host:port. The rest of the setup — including page.authenticate() — works the same. SOCKS5 proxies can handle both HTTP and HTTPS traffic and are sometimes faster than HTTP proxies because they operate at a lower network layer.
How do I use a different proxy per page in the same browser?
Chromium’s --proxy-server flag is browser-wide — all pages share the same proxy. To use different proxies per page, you need to launch separate browser instances (one per proxy) or use a proxy management library like proxy-chain that creates a local proxy server to forward individual requests to different upstream proxies.
Will a residential proxy alone prevent bot detection?
No. A residential proxy masks your IP, but modern anti-bot systems also fingerprint your browser (canvas, WebGL, fonts, plugin list, navigator.webdriver). Puppeteer’s default Chromium leaks automation signals. You need stealth plugins, headless detection bypasses, or an antidetect browser profile for full coverage.
How much bandwidth does Puppeteer use with residential proxies?
A typical page load consumes 1-5MB. With resource blocking (images, fonts, stylesheets), consumption drops to 200-500KB per page. If you’re scraping 1,000 pages per day with resource blocking, expect roughly 200MB-500MB of daily bandwidth. Most residential proxy providers bill by the GB, so resource blocking directly reduces your costs.
What’s the best proxy rotation interval for avoiding detection?
There’s no universal answer — it depends on the target site’s anti-bot system. As a starting point: rotate IPs every 5-10 requests for general scraping, and use sticky sessions (same IP for 5-10 minutes) when you need to maintain a session across multiple page navigations on the same site. Monitor your block rate and adjust accordingly.
Can I use free residential proxies with Puppeteer?
Technically yes, but don’t. Free proxies are overwhelmingly unreliable (high failure rates, extreme latency), frequently blacklisted (the target site has already blocked them), and a security risk (the proxy operator can inspect and modify your traffic). For any production or semi-serious use case, paid residential proxies are a requirement.
How do I check if my Puppeteer proxy is actually working?
Navigate to https://httpbin.org/ip or https://api.ipify.org after setting up the proxy. The returned IP should be different from your server’s real IP. You can also check geo-location by visiting https://ipinfo.io through the proxy to verify the IP resolves to the expected country and city.
Why does my Puppeteer script work locally but fail on a server?
Common causes: (1) the server’s firewall blocks outbound connections to the proxy port — whitelist the port, (2) the server runs headless Linux without the required shared libraries for Chromium — install them with apt install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libxcomposite1 libxdamage1, or (3) the server’s own IP is already flagged, and the proxy provider is rejecting connections from datacenter IPs — contact your proxy provider to whitelist your server.