How to Use Proxies with Puppeteer in 2026: Complete Guide

How to Use Proxies with Puppeteer in 2026: Complete Guide

Puppeteer remains one of the most widely used headless browser libraries in the Node.js ecosystem. Whether you are scraping product data, running automated tests, or monitoring competitor websites, proxies are essential for avoiding IP blocks and accessing geo-restricted content.

This guide walks through every proxy configuration method available in Puppeteer, from basic setup to advanced rotation strategies, with real code you can copy and run immediately.

Why Proxies Matter for Puppeteer

When Puppeteer launches Chromium, every HTTP request originates from your server’s IP address. Without proxies:

  • Target websites will rate-limit or ban your IP after a few hundred requests
  • You cannot access content restricted to specific geographic regions
  • Your server IP gets flagged in abuse databases
  • Concurrent scraping sessions all share the same IP, making detection trivial

Proxies solve all of these problems. If you need to estimate proxy bandwidth costs for your project, check out our proxy cost calculator.

Prerequisites

  • Node.js 18+ installed
  • npm or yarn package manager
  • A proxy provider with HTTP/HTTPS or SOCKS5 endpoints

Install Puppeteer

npm init -y
npm install puppeteer

This installs Puppeteer along with a bundled Chromium binary.

Basic Proxy Setup via Launch Args

Puppeteer passes command-line arguments directly to Chromium. The --proxy-server flag is how you configure a proxy:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    args: ['--proxy-server=http://proxy.example.com:8080']
  });

  const page = await browser.newPage();
  await page.goto('https://httpbin.org/ip');
  const body = await page.$eval('body', el => el.textContent);
  console.log('IP:', body);

  await browser.close();
})();

SOCKS5 Proxy

const browser = await puppeteer.launch({
  args: ['--proxy-server=socks5://proxy.example.com:1080']
});

Bypassing Proxy for Specific Domains

const browser = await puppeteer.launch({
  args: [
    '--proxy-server=http://proxy.example.com:8080',
    '--proxy-bypass-list=localhost,127.0.0.1,*.internal.com'
  ]
});

Authenticated Proxy Handling

Chromium’s --proxy-server flag does not support inline credentials (unlike cURL). Puppeteer provides page.authenticate() for this:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    args: ['--proxy-server=http://proxy.example.com:8080']
  });

  const page = await browser.newPage();

  // Set proxy credentials BEFORE navigating
  await page.authenticate({
    username: 'your_username',
    password: 'your_password'
  });

  await page.goto('https://httpbin.org/ip');
  const body = await page.$eval('body', el => el.textContent);
  console.log('IP:', body);

  await browser.close();
})();

Important: Call page.authenticate() before any page.goto() call. If you call it after navigation has started, authentication will fail for that request.

Per-Page Proxy Switching

Puppeteer sets the proxy at the browser level, which means all pages share the same proxy. To use different proxies for different pages, you need to launch separate browser instances:

const puppeteer = require('puppeteer');

async function scrapeWithProxy(url, proxy, credentials) {
  const browser = await puppeteer.launch({
    args: [`--proxy-server=${proxy}`]
  });

  const page = await browser.newPage();

  if (credentials) {
    await page.authenticate(credentials);
  }

  try {
    await page.goto(url, { timeout: 30000, waitUntil: 'domcontentloaded' });
    const title = await page.title();
    return { url, title, proxy, status: 'success' };
  } catch (err) {
    return { url, error: err.message, proxy, status: 'failed' };
  } finally {
    await browser.close();
  }
}

// Usage
(async () => {
  const tasks = [
    { url: 'https://example.com', proxy: 'http://proxy1.example.com:8080' },
    { url: 'https://example.org', proxy: 'http://proxy2.example.com:8080' },
  ];

  const results = await Promise.all(
    tasks.map(t => scrapeWithProxy(t.url, t.proxy, {
      username: 'user',
      password: 'pass'
    }))
  );

  console.log(results);
})();

Alternative: Request Interception for Per-Request Proxy

For finer control, you can intercept requests and route them through different proxies using a local proxy server like proxy-chain:

npm install proxy-chain
const puppeteer = require('puppeteer');
const proxyChain = require('proxy-chain');

(async () => {
  // Create an anonymized proxy URL (handles authentication)
  const proxyUrl = await proxyChain.anonymizeProxy(
    'http://username:password@proxy.example.com:8080'
  );

  const browser = await puppeteer.launch({
    args: [`--proxy-server=${proxyUrl}`]
  });

  const page = await browser.newPage();
  await page.goto('https://httpbin.org/ip');
  console.log(await page.$eval('body', el => el.textContent));

  await browser.close();
  await proxyChain.closeAnonymizedProxy(proxyUrl);
})();

The proxy-chain library is especially useful because it handles authentication transparently, so you do not need page.authenticate().

Using puppeteer-extra with Stealth Plugin

The puppeteer-extra ecosystem provides plugins that make Puppeteer harder to detect. The stealth plugin patches numerous fingerprinting vectors:

npm install puppeteer-extra puppeteer-extra-plugin-stealth
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

(async () => {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--proxy-server=http://proxy.example.com:8080',
      '--disable-blink-features=AutomationControlled',
      '--no-sandbox'
    ]
  });

  const page = await browser.newPage();

  await page.authenticate({
    username: 'user',
    password: 'pass'
  });

  // Set a realistic viewport and user agent
  await page.setViewport({ width: 1920, height: 1080 });
  await page.setUserAgent(
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
  );

  await page.goto('https://bot.sannysoft.com');
  await page.screenshot({ path: 'stealth-results.png', fullPage: true });

  await browser.close();
})();

Verify that your stealth configuration works by testing against our browser fingerprint tester.

Proxy Rotation Strategies

Strategy 1: Round-Robin Rotation

Cycle through a list of proxies sequentially:

const puppeteer = require('puppeteer');

class ProxyRotator {
  constructor(proxies) {
    this.proxies = proxies;
    this.index = 0;
  }

  next() {
    const proxy = this.proxies[this.index];
    this.index = (this.index + 1) % this.proxies.length;
    return proxy;
  }

  remove(proxy) {
    this.proxies = this.proxies.filter(p => p.server !== proxy.server);
    if (this.index >= this.proxies.length) this.index = 0;
  }
}

(async () => {
  const rotator = new ProxyRotator([
    { server: 'http://proxy1.example.com:8080', username: 'user', password: 'pass' },
    { server: 'http://proxy2.example.com:8080', username: 'user', password: 'pass' },
    { server: 'http://proxy3.example.com:8080', username: 'user', password: 'pass' },
  ]);

  const urls = [
    'https://example.com/page1',
    'https://example.com/page2',
    'https://example.com/page3',
    'https://example.com/page4',
    'https://example.com/page5',
  ];

  for (const url of urls) {
    const proxy = rotator.next();
    const browser = await puppeteer.launch({
      args: [`--proxy-server=${proxy.server}`]
    });

    const page = await browser.newPage();
    await page.authenticate({ username: proxy.username, password: proxy.password });

    try {
      await page.goto(url, { timeout: 20000 });
      console.log(`[${proxy.server}] ${url}: ${await page.title()}`);
    } catch (err) {
      console.error(`[${proxy.server}] ${url}: ${err.message}`);
    }

    await browser.close();
  }
})();

Strategy 2: Rotating Gateway Proxy

Use a single gateway endpoint from your proxy provider. The provider handles rotation server-side:

const browser = await puppeteer.launch({
  args: ['--proxy-server=http://gate.provider.com:7777']
});

const page = await browser.newPage();
await page.authenticate({
  username: 'user-country-us-session-' + Date.now(),
  password: 'password'
});

By appending a unique session ID to the username, most providers assign a fresh IP per session.

Strategy 3: Concurrent Pool with Browser Reuse

const puppeteer = require('puppeteer');
const proxyChain = require('proxy-chain');

async function createPool(proxies, concurrency = 3) {
  const pool = [];

  for (let i = 0; i < concurrency; i++) {
    const proxy = proxies[i % proxies.length];
    const anonUrl = await proxyChain.anonymizeProxy(
      `http://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}`
    );

    const browser = await puppeteer.launch({
      args: [`--proxy-server=${anonUrl}`]
    });

    pool.push({ browser, anonUrl, proxy });
  }

  return pool;
}

async function destroyPool(pool) {
  for (const { browser, anonUrl } of pool) {
    await browser.close();
    await proxyChain.closeAnonymizedProxy(anonUrl);
  }
}

// Usage
(async () => {
  const proxies = [
    { host: 'proxy1.example.com', port: 8080, username: 'user', password: 'pass' },
    { host: 'proxy2.example.com', port: 8080, username: 'user', password: 'pass' },
    { host: 'proxy3.example.com', port: 8080, username: 'user', password: 'pass' },
  ];

  const pool = await createPool(proxies, 3);
  let poolIndex = 0;

  const urls = Array.from({ length: 10 }, (_, i) => `https://example.com/page${i + 1}`);

  for (const url of urls) {
    const { browser } = pool[poolIndex % pool.length];
    poolIndex++;

    const page = await browser.newPage();
    try {
      await page.goto(url, { timeout: 20000 });
      console.log(`${url}: ${await page.title()}`);
    } catch (err) {
      console.error(`${url}: ${err.message}`);
    } finally {
      await page.close();
    }
  }

  await destroyPool(pool);
})();

Error Handling and Timeout Management

Robust error handling is critical when working with proxies:

const puppeteer = require('puppeteer');

async function scrapeWithRetry(url, proxyConfig, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    let browser;
    try {
      browser = await puppeteer.launch({
        args: [`--proxy-server=${proxyConfig.server}`],
        timeout: 15000 // Browser launch timeout
      });

      const page = await browser.newPage();
      await page.authenticate({
        username: proxyConfig.username,
        password: proxyConfig.password
      });

      // Set navigation timeout
      page.setDefaultNavigationTimeout(25000);

      // Set up request failure logging
      page.on('requestfailed', request => {
        if (request.url() === url) {
          console.warn(`Request failed: ${request.failure()?.errorText}`);
        }
      });

      const response = await page.goto(url, { waitUntil: 'domcontentloaded' });

      if (!response) {
        throw new Error('No response received');
      }

      if (response.status() === 403 || response.status() === 429) {
        throw new Error(`Blocked with status ${response.status()}`);
      }

      const content = await page.content();
      return { success: true, content, status: response.status() };

    } catch (err) {
      console.error(`Attempt ${attempt}/${maxRetries} failed: ${err.message}`);

      if (attempt === maxRetries) {
        return { success: false, error: err.message };
      }

      // Exponential backoff
      const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
      await new Promise(resolve => setTimeout(resolve, delay));
    } finally {
      if (browser) await browser.close();
    }
  }
}

// Usage
(async () => {
  const result = await scrapeWithRetry('https://example.com', {
    server: 'http://proxy.example.com:8080',
    username: 'user',
    password: 'pass'
  });

  if (result.success) {
    console.log(`Got ${result.content.length} bytes`);
  } else {
    console.error('All retries failed:', result.error);
  }
})();

Blocking Unnecessary Resources

Save proxy bandwidth by intercepting and blocking requests for images, fonts, and analytics:

await page.setRequestInterception(true);

page.on('request', (request) => {
  const blockedTypes = ['image', 'font', 'media', 'stylesheet'];
  const blockedDomains = ['google-analytics.com', 'facebook.net', 'doubleclick.net'];

  const url = request.url();
  const type = request.resourceType();

  if (blockedTypes.includes(type) || blockedDomains.some(d => url.includes(d))) {
    request.abort();
  } else {
    request.continue();
  }
});

Troubleshooting Common Issues

Error: net::ERR_PROXY_CONNECTION_FAILED

Cause: Proxy server is down or the address is incorrect.

Fix:

  1. Test the proxy with cURL: curl -x http://proxy:port https://httpbin.org/ip
  2. Check that the proxy port is correct
  3. Verify your IP is whitelisted with the proxy provider

Error: net::ERR_PROXY_AUTH_FAILED

Cause: Wrong credentials or page.authenticate() called after navigation.

Fix: Always call page.authenticate() before page.goto(). Double-check your username and password.

Page Loads but Shows CAPTCHA

Cause: The proxy IP is flagged or you are detected as a bot.

Fix:

  • Use residential proxies instead of datacenter proxies
  • Add the stealth plugin (puppeteer-extra-plugin-stealth)
  • Rotate proxies more frequently
  • Add realistic delays between actions: await page.waitForTimeout(2000 + Math.random() * 3000)

Memory Leaks with Many Browser Instances

Cause: Not closing browser instances properly.

Fix: Always call browser.close() in a finally block. For long-running scripts, monitor memory usage:

const used = process.memoryUsage();
console.log(`Memory: RSS=${Math.round(used.rss / 1024 / 1024)}MB, Heap=${Math.round(used.heapUsed / 1024 / 1024)}MB`);

Proxy Works in cURL but Not in Puppeteer

Cause: The proxy may require a specific protocol or TLS version that Chromium handles differently.

Fix: Try using proxy-chain to create a local proxy wrapper:

const proxyUrl = await proxyChain.anonymizeProxy('http://user:pass@proxy:port');

Summary

Puppeteer’s proxy support works through Chromium’s command-line flags, which is straightforward but has limitations — particularly around per-page proxy switching and authentication. Key takeaways:

  • Use --proxy-server launch arg for basic proxy setup
  • Use page.authenticate() for proxy credentials (call it before navigation)
  • Use proxy-chain for cleaner authentication handling and anonymization
  • Use puppeteer-extra with the stealth plugin to avoid detection
  • Always implement retry logic with exponential backoff
  • Close browser instances properly to prevent memory leaks

For cost estimation, use our proxy cost calculator. To verify your bot fingerprint, try the browser fingerprint tester.


Related Reading

Scroll to Top