Proxy Setup Guide for Puppeteer and Playwright (Node.js)
Puppeteer and Playwright are the two dominant headless browser automation libraries for Node.js. Both support proxy configuration, but they handle it differently -- Puppeteer sets the proxy at browser launch, while Playwright supports per-context proxy configuration. This guide covers both libraries side by side with complete code examples for authentication, rotation, error handling, and anti-detection.
Every code example in this guide uses `gate.hexproxies.com:8080` as the proxy endpoint. Replace the credentials with your actual username and password.
---
Quick Answer
**Puppeteer** sets proxies via Chromium launch arguments (`--proxy-server`). Authentication requires intercepting requests with `page.authenticate()`. **Playwright** configures proxies per browser context, supporting both launch-level and context-level proxy with built-in authentication. For proxy rotation, Playwright is more flexible because you can create new contexts with different proxies without relaunching the browser. Both libraries need explicit error handling for proxy timeouts, auth failures, and blocked responses.
---
Installation
Set up a fresh project with both libraries:
mkdir proxy-automation && cd proxy-automation
npm init -y
npm install puppeteer playwright
npx playwright install chromiumPuppeteer downloads its own Chromium binary. Playwright requires an explicit install step for browser binaries.
---
Basic Proxy Configuration: Side by Side
Puppeteer: Launch-Level Proxy
Puppeteer configures the proxy server as a Chromium launch argument. Authentication is handled separately via `page.authenticate()`.
// puppeteer-basic-proxy.mjsconst PROXY = 'gate.hexproxies.com:8080'; const USERNAME = 'your-username'; const PASSWORD = 'your-password';
async function main() { const browser = await puppeteer.launch({ headless: 'new', args: [ '--proxy-server=http://' + PROXY, '--disable-blink-features=AutomationControlled', ], });
const page = await browser.newPage();
// Authenticate with the proxy await page.authenticate({ username: USERNAME, password: PASSWORD, });
// Verify proxy is working await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' }); const body = await page.evaluate(() => document.body.innerText); console.log('Puppeteer IP:', body);
await browser.close(); }
main(); ```
Playwright: Context-Level Proxy
Playwright supports proxy configuration at both browser and context level. Context-level is preferred because it enables rotation without relaunching.
// playwright-basic-proxy.mjsconst PROXY = 'gate.hexproxies.com:8080'; const USERNAME = 'your-username'; const PASSWORD = 'your-password';
async function main() { const browser = await chromium.launch({ headless: true });
// Proxy is set per context -- no browser restart needed const context = await browser.newContext({ proxy: { server: 'http://' + PROXY, username: USERNAME, password: PASSWORD, }, });
const page = await context.newPage();
await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' }); const body = await page.textContent('body'); console.log('Playwright IP:', body);
await context.close(); await browser.close(); }
main(); ```
Key Differences
| Feature | Puppeteer | Playwright | |---|---|---| | Proxy scope | Browser-level (launch args) | Context-level (per context) | | Authentication | `page.authenticate()` call | Built into context proxy config | | Rotation | Requires browser relaunch | New context with different proxy | | Protocol support | HTTP, HTTPS, SOCKS5 | HTTP, HTTPS, SOCKS5 | | Multiple browsers | Chromium only (default) | Chromium, Firefox, WebKit |
---
Proxy Rotation Within Browser Context
For scraping multiple pages through different proxy IPs, you need rotation. The approach differs significantly between Puppeteer and Playwright.
Puppeteer: Rotation via Browser Relaunch
Because Puppeteer sets the proxy at launch time, rotating requires closing and relaunching the browser. This is slower but straightforward.
// puppeteer-rotation.mjsconst PROXIES = [ { host: 'gate.hexproxies.com:8080', user: 'user-session-1', pass: 'your-password' }, { host: 'gate.hexproxies.com:8080', user: 'user-session-2', pass: 'your-password' }, { host: 'gate.hexproxies.com:8080', user: 'user-session-3', pass: 'your-password' }, ];
async function scrapeWithProxy(url, proxy) { const browser = await puppeteer.launch({ headless: 'new', args: ['--proxy-server=http://' + proxy.host], });
try { 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.content(); return { url, success: true, length: content.length }; } catch (error) { return { url, success: false, error: error.message }; } finally { await browser.close(); } }
async function main() { const urls = [ 'https://example.com/page-1', 'https://example.com/page-2', 'https://example.com/page-3', ];
const results = []; for (let i = 0; i < urls.length; i++) { const proxy = PROXIES[i % PROXIES.length]; const result = await scrapeWithProxy(urls[i], proxy); results.push(result); console.log(result); } }
main(); ```
Playwright: Rotation via New Contexts
Playwright can create new contexts with different proxies on the same browser instance. This is significantly faster because browser launch is the most expensive operation.
// playwright-rotation.mjsconst PROXIES = [ { server: 'http://gate.hexproxies.com:8080', username: 'user-session-1', password: 'your-password' }, { server: 'http://gate.hexproxies.com:8080', username: 'user-session-2', password: 'your-password' }, { server: 'http://gate.hexproxies.com:8080', username: 'user-session-3', password: 'your-password' }, ];
async function scrapeWithContext(browser, url, proxy) { const context = await browser.newContext({ proxy });
try { const page = await context.newPage(); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); const content = await page.content(); return { url, success: true, length: content.length }; } catch (error) { return { url, success: false, error: error.message }; } finally { await context.close(); } }
async function main() { const browser = await chromium.launch({ headless: true });
const urls = [ 'https://example.com/page-1', 'https://example.com/page-2', 'https://example.com/page-3', ];
const results = []; for (let i = 0; i < urls.length; i++) { const proxy = PROXIES[i % PROXIES.length]; const result = await scrapeWithContext(browser, urls[i], proxy); results.push(result); console.log(result); }
await browser.close(); }
main(); ```
Performance Comparison
| Operation | Puppeteer (relaunch) | Playwright (new context) | |---|---|---| | Browser launch | ~800--1,200 ms | One-time: ~800--1,200 ms | | Per-rotation overhead | ~800--1,200 ms (full relaunch) | ~50--100 ms (context only) | | 100 rotations | ~100 seconds overhead | ~7 seconds overhead | | Memory per instance | ~80--150 MB per browser | ~20--40 MB per context |
For high-volume rotation, Playwright's context-based approach is roughly 14x faster in rotation overhead.
---
Error Handling for Proxy Failures
Proxy connections fail. Networks time out. IPs get banned. Robust error handling is not optional.
Common Failure Modes
- **Connection timeout:** Proxy gateway is unreachable or overloaded (ETIMEDOUT, ECONNREFUSED)
- **Authentication failure:** Wrong credentials or expired session (HTTP 407)
- **Target blocks the IP:** Anti-bot returns CAPTCHA, 403, or empty response
- **Proxy returns stale data:** Cached response from proxy infrastructure
Puppeteer Error Handling with Retry
// puppeteer-error-handling.mjsconst PROXY = 'gate.hexproxies.com:8080'; const USERNAME = 'your-username'; const PASSWORD = 'your-password'; const MAX_RETRIES = 3; const RETRY_DELAY_MS = 2000;
function isBlockedResponse(content) { const lowerContent = content.toLowerCase(); return ( lowerContent.includes('captcha') || lowerContent.includes('access denied') || lowerContent.includes('blocked') || lowerContent.includes('challenge-platform') ); }
async function wait(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); }
async function scrapeWithRetry(url, retries) { const maxAttempts = retries ?? MAX_RETRIES;
for (let attempt = 1; attempt <= maxAttempts; attempt++) { let browser = null;
try { browser = await puppeteer.launch({ headless: 'new', args: ['--proxy-server=http://' + PROXY], });
const page = await browser.newPage(); await page.authenticate({ username: USERNAME, password: PASSWORD });
// Set navigation timeout page.setDefaultNavigationTimeout(20000);
const response = await page.goto(url, { waitUntil: 'domcontentloaded', });
if (!response || response.status() >= 400) { throw new Error('HTTP ' + (response ? response.status() : 'no response')); }
const content = await page.content();
if (isBlockedResponse(content)) { throw new Error('Blocked: CAPTCHA or challenge detected'); }
return { success: true, content, attempt }; } catch (error) { console.error( 'Attempt ' + attempt + '/' + maxAttempts + ' failed: ' + error.message );
if (attempt < maxAttempts) { await wait(RETRY_DELAY_MS * attempt); } } finally { if (browser) { await browser.close(); } } }
return { success: false, content: null, attempt: maxAttempts }; } ```
Playwright Error Handling with Proxy Failover
// playwright-error-handling.mjsconst PROXY_POOL = [ { server: 'http://gate.hexproxies.com:8080', username: 'user-session-a', password: 'your-password' }, { server: 'http://gate.hexproxies.com:8080', username: 'user-session-b', password: 'your-password' }, { server: 'http://gate.hexproxies.com:8080', username: 'user-session-c', password: 'your-password' }, ];
function isBlockedResponse(content) { const lowerContent = content.toLowerCase(); return ( lowerContent.includes('captcha') || lowerContent.includes('access denied') || lowerContent.includes('blocked') || lowerContent.includes('challenge-platform') ); }
async function scrapeWithFailover(browser, url) { for (let i = 0; i < PROXY_POOL.length; i++) { const proxy = PROXY_POOL[i]; const context = await browser.newContext({ proxy, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', });
try { const page = await context.newPage();
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000, });
if (!response || response.status() >= 400) { throw new Error('HTTP ' + (response ? response.status() : 'no response')); }
const content = await page.content();
if (isBlockedResponse(content)) { throw new Error('Blocked: CAPTCHA or challenge detected'); }
return { success: true, content, proxyIndex: i }; } catch (error) { console.error( 'Proxy ' + (i + 1) + '/' + PROXY_POOL.length + ' failed for ' + url + ': ' + error.message ); } finally { await context.close(); } }
return { success: false, content: null, proxyIndex: -1 }; }
async function main() { const browser = await chromium.launch({ headless: true });
const result = await scrapeWithFailover(browser, 'https://example.com'); console.log('Result:', { success: result.success, contentLength: result.content ? result.content.length : 0, proxyUsed: result.proxyIndex + 1, });
await browser.close(); }
main(); ```
---
Combining Proxies with Fingerprint Randomization
A proxy alone is not enough for sophisticated anti-bot systems. You also need to randomize browser fingerprint attributes. Here is a complete example combining proxy rotation with fingerprint randomization in Playwright.
// playwright-stealth-proxy.mjsconst PROXY = { server: 'http://gate.hexproxies.com:8080', username: 'your-username', password: 'your-password', };
const USER_AGENTS = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', ];
const VIEWPORTS = [ { width: 1920, height: 1080 }, { width: 1366, height: 768 }, { width: 1536, height: 864 }, { width: 1440, height: 900 }, ];
const TIMEZONES = [ 'America/New_York', 'America/Chicago', 'America/Los_Angeles', 'Europe/London', ];
function randomItem(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
async function createStealthContext(browser) { return browser.newContext({ proxy: PROXY, userAgent: randomItem(USER_AGENTS), viewport: randomItem(VIEWPORTS), timezoneId: randomItem(TIMEZONES), locale: 'en-US', permissions: [], javaScriptEnabled: true, }); }
async function main() { const browser = await chromium.launch({ headless: true });
// Each context gets a unique fingerprint + proxy session for (let i = 0; i < 5; i++) { const context = await createStealthContext(browser); const page = await context.newPage();
// Mask automation indicators await page.addInitScript(() => { Object.defineProperty(navigator, 'webdriver', { get: () => false }); });
await page.goto('https://httpbin.org/headers', { waitUntil: 'domcontentloaded', });
const headers = await page.textContent('body'); console.log('Request ' + (i + 1) + ':', headers);
await context.close(); }
await browser.close(); }
main(); ```
---
SOCKS5 Proxy Configuration
Both libraries support SOCKS5 proxies for protocols beyond HTTP/HTTPS.
Puppeteer with SOCKS5
const browser = await puppeteer.launch({
args: ['--proxy-server=socks5://gate.hexproxies.com:8080'],
});Playwright with SOCKS5
const context = await browser.newContext({
proxy: {
server: 'socks5://gate.hexproxies.com:8080',
username: 'your-username',
password: 'your-password',
},
});---
Troubleshooting Common Issues
ERR_PROXY_CONNECTION_FAILED
The browser cannot reach the proxy gateway. Check that the proxy host and port are correct, your firewall allows outbound connections on the proxy port, and the proxy provider's gateway is operational.
ERR_TUNNEL_CONNECTION_FAILED
The proxy connected but cannot reach the target site. This usually means the proxy IP is blocked by the target, or the target site is down. Switch to a different proxy IP and retry.
Slow Page Loads Through Proxy
If pages load slowly through the proxy but fast without it, check your proxy latency with a simple HTTP request (not a full browser load). If the HTTP request is fast but browser loads are slow, the bottleneck is likely resource loading -- the browser loads dozens of sub-resources (CSS, JS, images) through the proxy. Consider blocking unnecessary resources:
// Block images, fonts, and media to speed up scraping
await page.route('**/*.{png,jpg,jpeg,gif,svg,woff,woff2,mp4}', (route) =>
route.abort()
);Memory Leaks with Browser Instances
If running many iterations, ensure you close both contexts and browsers. A common mistake is closing the page but not the context, which leaks memory over time. Always use try/finally blocks to guarantee cleanup.
---
Frequently Asked Questions
**Should I use Puppeteer or Playwright for proxy automation?** Playwright is generally better for proxy-heavy workloads because it supports per-context proxy configuration, which makes rotation ~14x faster than Puppeteer's relaunch approach. Puppeteer is fine for simple scripts with a single proxy.
**Can I use both HTTP and SOCKS5 proxies?** Yes. Both libraries support HTTP, HTTPS, and SOCKS5 proxy protocols. SOCKS5 is useful when you need to proxy non-HTTP traffic or when your target requires UDP support.
**How many concurrent browser contexts can I run?** This depends on your server's RAM. Each Chromium context uses ~20--40 MB. A server with 8 GB RAM can comfortably run 100--150 concurrent contexts. Monitor memory usage and set a concurrency limit accordingly.
**Do I need a stealth plugin?** For basic scraping, the fingerprint randomization shown in this guide is sufficient. For heavily protected sites, consider puppeteer-extra-plugin-stealth (Puppeteer) or playwright-stealth (Playwright) for more comprehensive anti-detection.