v1.8.91-d84675c
← Back to Hex Proxies

Proxy Setup Guide for Puppeteer and Playwright (Node.js)

Last updated: April 2026

By Hex Proxies Engineering Team

A complete guide to proxy configuration in Puppeteer and Playwright for Node.js. Includes side-by-side code examples for authentication, proxy rotation within browser contexts, error handling for proxy failures, and anti-detection techniques.

intermediate14 minutesdeveloper-guides

Prerequisites

  • Node.js installed (v18 or later)
  • Basic understanding of async/await in JavaScript
  • npm or yarn package manager

Steps

1

Install Puppeteer and Playwright

Set up your Node.js project and install both libraries to follow the side-by-side examples.

2

Configure basic proxy authentication

Learn the two proxy authentication patterns: launch-time args (Puppeteer) and per-context proxy (Playwright).

3

Implement proxy rotation in browser context

Build a rotation layer that switches proxies between pages or browser contexts without restarting the browser.

4

Add error handling for proxy failures

Handle connection timeouts, authentication failures, and blocked responses with retry logic and proxy failover.

5

Combine proxies with fingerprint randomization

Pair proxy rotation with viewport, user-agent, and timezone randomization for maximum anti-detection.

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 chromium

Puppeteer 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.mjs

const 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.mjs

const 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.mjs

const 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.mjs

const 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

  1. **Connection timeout:** Proxy gateway is unreachable or overloaded (ETIMEDOUT, ECONNREFUSED)
  2. **Authentication failure:** Wrong credentials or expired session (HTTP 407)
  3. **Target blocks the IP:** Anti-bot returns CAPTCHA, 403, or empty response
  4. **Proxy returns stale data:** Cached response from proxy infrastructure

Puppeteer Error Handling with Retry

// puppeteer-error-handling.mjs

const 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.mjs

const 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.mjs

const 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.

Tips

  • *Playwright supports per-context proxies natively, making rotation easier than Puppeteer which requires launch-level proxy args.
  • *Always set a page-level timeout separate from the proxy timeout. A proxy may connect but the target may be slow.
  • *Use headless: "new" in Puppeteer (not headless: true) to use the new headless mode that is harder for sites to detect.
  • *Rotate user agents alongside proxies. A single user agent from 100 different IPs is an obvious automation signal.
  • *Test your proxy setup against httpbin.org/ip first to confirm the proxy is working before hitting your target site.

Ready to Get Started?

Put this guide into practice with Hex Proxies.

Cookie Preferences

We use cookies to ensure the best experience. You can customize your preferences below. Learn more