Back to blog

How To Use a Proxy in Puppeteer: Setup, Rotation, and Authentication

Share article:

Puppeteer is a Node.js library that controls headless Chromium for browser automation and web scraping. Without proxies, every request goes out from your real IP, and on protected sites, that IP gets blocked fast. This guide covers every method for configuring, authenticating, and rotating proxies in Puppeteer, plus how to troubleshoot the failures you'll actually run into. Not familiar with how proxies work? Check out what a proxy server is before continuing.

A geometric icon resembling a classical building: a triangle at the top like a roof, four evenly spaced vertical lines underneath resembling pillars, and a horizontal line at the bottom forming a base.

TL;DR

  • Puppeteer automates headless Chromium for web scraping and browser testing, but without proxies, every request exposes your real IP, leading to blocks, CAPTCHAs, and geo-restrictions on protected sites
  • This guide walks through setting up static and rotating proxies in Puppeteer using Node.js, covering 3 authentication methods (page.authenticate()proxy-chain, and the Proxy-Authorization header), 3 rotation strategies, and advanced configurations including per-page proxies, proxy chaining, domain bypass, and bandwidth reduction.
  • Chrome's --proxy-server flag silently ignores inline credentials; always authenticate using page.authenticate() or the proxy-chain package; install proxy-chain v2.x specifically, since v3.x dropped CommonJS support and breaks require()
  • Before writing any Puppeteer code, verify your proxy credentials work independently with curl — if curl returns a 407 or connection error, the problem is your credentials or provider, not your script
  • Prerequisites are Node.js v18+, npm, a Puppeteer installation (npm install puppeteer), and proxy credentials from your provider (hostname, port, username, password)

Prerequisites and environment setup

Step 1: Install Node.js and npm

You need Node.js v18 or higher. Download it from the official Node.js site and run the installer.

On Windows, check the box that adds Node to your PATH during installation. On macOS, you can use the installer or run:

brew install node

Verify the install:

node -v
npm -v

If you need a refresher on Node.js, check out our Node.js glossary entry.

Step 2: Create the project

Run the following commands one after the other to create your Node.js project and install Puppeteer:

mkdir puppeteer-proxy-guide
cd puppeteer-proxy-guide
npm init -y
npm install puppeteer

During installation, Puppeteer downloads a bundled Chromium binary. The installation may take a while. 

If you want to use an existing Chrome or Chromium installation instead, install puppeteer-core and pass executablePath to every puppeteer.launch() call:

npm install puppeteer-core

Learn more about what headless browsers are and how they work: what is a headless browser? and headless Chrome.

Step 3: Verify the setup

Create a file named verify.js in your project folder and paste in the following code:

const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "verify.png" });
console.log("Screenshot saved. Setup is working.");
await browser.close();
})();

This confirms that Chromium launches correctly before you add any proxy logic. Run this command on the terminal to execute the file:

node verify.js

You should see verify.png appear in the project folder. On macOS, open it with open verify.png.

Step 4: Gather your proxy credentials

Before writing any proxy code, have these details ready from your Decodo’s dashboard:

  • Hostname: gate.decodo.com
  • Port: e.g., 10000
  • Username and password: for Decodo, you’ll find these in your dashboard after purchasing a proxy plan
  • Protocol: HTTP, HTTPS, or SOCKS5

How to set up a static proxy in Puppeteer

The --proxy-server flag is the foundation of proxy configuration in Puppeteer. Every method in this guide builds on it.

The code below sets up a static proxy in Puppeteer. Here, the --proxy-server flag is passed inside the args array of puppeteer.launch():

//filename: static-proxy.js
const puppeteer = require("puppeteer");
const PROXY = "http://gate.decodo.com:10000";
(async () => {
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${PROXY}`],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
})();

Since this script uses no authentication, it only works if you've whitelisted your IP in your Decodo dashboard. If you have, running the file (node static-proxy.js) returns your proxy's IP address instead of your real one: 

IP seen by target site: {
"origin": "185.181.109.4"
}

For SOCKS5 proxies, swap the protocol prefix:

const PROXY = "socks5://gate.decodo.com:10000";

Read on SOCKS5 vs. HTTP proxies to understand when to use each, and explore HTTP proxies in more depth.

The inline credentials trap

One of the most common proxy configuration mistakes in Puppeteer is assuming that Chrome accepts credentials embedded in the proxy URL. While this format looks valid, Chrome silently ignores the username and password:

// This will NOT authenticate — Chrome ignores the credentials
const PROXY = "http://username:password@gate.decodo.com:10000";

The tricky part is that Chrome doesn't throw an error when this happens. Instead, requests typically fail with a 407 Proxy Authentication Required response because the proxy never receives the authentication credentials.

The authentication methods covered in the next section provide the correct way to handle proxy authentication in Puppeteer.

How to verify that the proxy is working

Compare the IP returned by https://httpbin.org/ip with your actual public IP address. You can find your real IP by running the following command in your terminal.

Windows (Command Prompt):

curl https://httpbin.org/ip

macOS:

curl https://httpbin.org/ip

If the IP returned by Puppeteer matches your local IP, then the proxy is not being used. If the two IPs are different, the proxy is working correctly.

Free proxy lists won't cut it

Half are dead, the rest are flagged. Decodo's residential proxies give your Puppeteer scraper 115M+ clean IPs with automatic rotation. No list maintenance required.

How to authenticate proxies in Puppeteer

Chrome doesn’t support inline proxy credentials, so you need to use one of three alternative approaches. Below are the common methods and their tradeoffs.

Method 1: page.authenticate()

The page.authenticate() method is Puppeteer's built-in method for basic HTTP proxy authentication, and it must be called before page.goto() to work. Here’s the full code:

//filename: auth-page.js
const puppeteer = require("puppeteer");
const PROXY_HOST = "gate.decodo.com";
const PROXY_PORT = "10000";
const PROXY_USER = "YOUR_USERNAME";
const PROXY_PASS = "YOUR_PASSWORD";
(async () => {
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=http://${PROXY_HOST}:${PROXY_PORT}`],
});
const page = await browser.newPage();
// Must be called before page.goto()
await page.authenticate({
username: PROXY_USER,
password: PROXY_PASS,
});
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
})();

Limitation: page.authenticate() is scoped to a single page instance. Assuming you create a new page with browser.newPage(), you’ll need to call page.authenticate() again on that new page.

The proxy-chain package, developed by Apify, creates a local intermediary proxy server on 127.0.0.1 that handles authentication transparently. Chrome connects to the local address and never sees the real credentials.

For this tutorial, we’re installing v2.x, since v3.x dropped CommonJS support and will break require():

npm install proxy-chain@2
Here’s the full code for authenticating using this method:
// filename: auth-proxychain.js
const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_URL = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
(async () => {
// Returns a local address like http://127.0.0.1:59011
const anonymizedProxy = await proxyChain.anonymizeProxy(PROXY_URL);
console.log("Local proxy address:", anonymizedProxy);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
// Always clean up – closes the local proxy server
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();

When it's working, your console output looks like this:

Local proxy address: http://127.0.0.1:59011
IP seen by target site: {
"origin": "185.181.109.4"
}

Remember to always call closeAnonymizedProxy() after closing the browser. Skipping it leaves the local proxy server running and causes resource leaks over time.

If your password contains special characters like @, :, or /, encode them before building the URL:

const user = encodeURIComponent("YOUR_USERNAME");
const pass = encodeURIComponent("YOUR_PASSWORD");
const PROXY_URL = `http://${user}:${pass}@gate.decodo.com:10000`;

See Decodo’s authentication options to learn how we support both username/password authentication and IP whitelisting.

Method 3: Proxy-Authorization header

This approach uses page.setExtraHTTPHeaders() to send a Proxy-Authorization header containing Base64-encoded credentials. While it's worth knowing about, it has a major limitation: it only works with HTTP destinations and cannot authenticate proxy requests to HTTPS sites.

The reason is that HTTPS traffic is established through a CONNECT tunnel. Custom headers added with page.setExtraHTTPHeaders() are not forwarded during tunnel setup, so the proxy never receives the authentication credentials.

// filename: auth-header.js
const puppeteer = require("puppeteer");
const PROXY_HOST = "gate.decodo.com";
const PROXY_PORT = "10000";
const PROXY_USER = "YOUR_USERNAME";
const PROXY_PASS = "YOUR_PASSWORD";
const credentials = Buffer.from(`${PROXY_USER}:${PROXY_PASS}`).toString("base64");
(async () => {
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=http://${PROXY_HOST}:${PROXY_PORT}`],
});
const page = await browser.newPage();
await page.setExtraHTTPHeaders({
"Proxy-Authorization": `Basic ${credentials}`,
});
// HTTP only — won't work on https:// targets
await page.goto("http://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
})();

Use this approach only as a fallback for HTTP-only scenarios. For most use cases, page.authenticate() or proxy-chain are more reliable and work with both HTTP and HTTPS traffic.

Configuring rotating proxies in Puppeteer

IP rotation is one of the most effective ways to scale web scraping. Even residential IPs can be flagged or blocked if they generate too many requests from a single address. By rotating IPs, you can distribute traffic across multiple endpoints and reduce the risk of detection. 

If you're new to the concept, see What Are Rotating Proxies?IP Rotation, and Sticky vs. Rotating Sessions for a deeper understanding of how proxy rotation works and when to use different rotation strategies. 

Below are three common approaches and the scenarios where each is most effective.

Random rotation from a proxy list

Select a proxy at random from a pool each time you launch a browser instance. This simple approach works well for small to medium-sized proxy pools and general-purpose scraping workloads where precise traffic distribution across IPs isn't a priority.

// filename: rotate-random.js
const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXIES = [
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000",
];
const URLS_TO_SCRAPE = [
"https://httpbin.org/ip",
"https://httpbin.org/ip",
"https://httpbin.org/ip",
];
function getRandomProxy() {
return PROXIES[Math.floor(Math.random() * PROXIES.length)];
}
(async () => {
for (const url of URLS_TO_SCRAPE) {
const proxyUrl = getRandomProxy();
const anonymizedProxy = await proxyChain.anonymizeProxy(proxyUrl);
console.log(`Anonymized proxy: ${anonymizedProxy}`);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("Response:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
}
})();

Note that when testing with a single Decodo gateway endpoint, it’s fine to reuse the same URL across all array entries, as we will still assign a different exit IP for each connection.

Sequential (round-robin) rotation

Iterate through the proxy list sequentially using an index counter so each proxy is used in order before any are reused. This is useful when you need a predictable distribution and guaranteed coverage across the entire pool.

// filename: rotate-roundrobin.js
const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXIES = [
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000",
];
const URLS_TO_SCRAPE = [
"https://httpbin.org/ip",
"https://httpbin.org/ip",
"https://httpbin.org/ip",
];
(async () => {
for (let i = 0; i < URLS_TO_SCRAPE.length; i++) {
// Wraps around if there are more URLs than proxies
const proxyUrl = PROXIES[i % PROXIES.length];
const anonymizedProxy = await proxyChain.anonymizeProxy(proxyUrl);
console.log(`Request ${i + 1} — Anonymized proxy: ${anonymizedProxy}`);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.goto(URLS_TO_SCRAPE[i], { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("Response:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
}
})();

Using a rotating proxy gateway endpoint

The simplest option for production scraping is a single rotating proxy endpoint. Providers automatically assign a new exit IP for each request, removing the need for any client-side rotation logic. With our rotating proxies, you simply pass one gateway URL, and each connection is routed through a fresh IP server-side.

// filename: rotate-gateway.js
const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_URL = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
(async () => {
const anonymizedProxy = await proxyChain.anonymizeProxy(PROXY_URL);
console.log("Anonymized proxy:", anonymizedProxy);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
// Each request gets a different IP automatically
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP on request 1:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();

How often you rotate depends on the target site and the size of your proxy pool – there’s no one-size-fits-all rule.

  • Aggressive anti-bot sites (e.g., Google, Amazon, social platforms). rotate every request or every 2–3 requests.
  • Moderately protected sites. Rotate every few minutes or per session.
  • Lightly protected sites. Rotate every 10–20 requests.

In general, you should rotate more frequently when dealing with high data volumes, smaller proxy pools, or strict rate limits.

Advanced proxy configurations

Custom IP per page

Instead of assigning a single proxy per browser instance, you can route each page through a different proxy while running them in parallel. This is useful for simulating multiple users, testing geo-targeted content, or comparing regional pricing simultaneously.

The puppeteer-page-proxy package is often referenced for this use case. However, it is no longer compatible with recent Puppeteer versions and throws errors such as TypeError: useProxyPer[target.constructor.name] is not a function. A more reliable approach is to run separate browser instances and manage proxy assignment using proxy-chain.

// filename: advanced-per-page.js
const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_1 = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
const PROXY_2 = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
(async () => {
const anonymizedProxy1 = await proxyChain.anonymizeProxy(PROXY_1);
const anonymizedProxy2 = await proxyChain.anonymizeProxy(PROXY_2);
console.log("Proxy 1:", anonymizedProxy1);
console.log("Proxy 2:", anonymizedProxy2);
// Launch 2 browser instances, each with its own proxy
const [browser1, browser2] = await Promise.all([
puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy1}`],
}),
puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy2}`],
}),
]);
const [page1, page2] = await Promise.all([
browser1.newPage(),
browser2.newPage(),
]);
await Promise.all([
page1.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" }),
page2.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" }),
]);
const body1 = await page1.$eval("body", (el) => el.innerText);
const body2 = await page2.$eval("body", (el) => el.innerText);
console.log("Page 1 IP:", body1);
console.log("Page 2 IP:", body2);
await Promise.all([browser1.close(), browser2.close()]);
await Promise.all([
proxyChain.closeAnonymizedProxy(anonymizedProxy1, true),
proxyChain.closeAnonymizedProxy(anonymizedProxy2, true),
]);
})();

Proxy chaining with proxy-chain

Proxy chaining routes your traffic through a local proxy server that forwards requests to one or more upstream proxies. This approach is useful when you want to add an extra layer of anonymity or route through multiple jurisdictions.

// filename: advanced-chaining.js
const puppeteer = require("puppeteer");
const ProxyChain = require("proxy-chain");
const UPSTREAM_PROXY = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
(async () => {
const server = new ProxyChain.Server({
port: 8099,
prepareRequestFunction: () => {
return { upstreamProxyUrl: UPSTREAM_PROXY };
},
});
await new Promise((resolve) => server.listen(resolve));
console.log("Local proxy server running on port 8099");
const browser = await puppeteer.launch({
headless: "new",
args: ["--proxy-server=http://127.0.0.1:8099"],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
await new Promise((resolve) => server.close(resolve));
})();

Bypassing the proxy for specific domains

Use --proxy-bypass-list to exclude certain domains from proxy routing. This is useful for local APIs, trusted CDNs, or internal services where routing traffic through a proxy adds unnecessary latency or complexity.

// filename: advanced-bypass.js
const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_URL = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
(async () => {
const anonymizedProxy = await proxyChain.anonymizeProxy(PROXY_URL);
const browser = await puppeteer.launch({
headless: "new",
args: [
`--proxy-server=${anonymizedProxy}`,
// Comma-separated — these domains bypass the proxy entirely
"--proxy-bypass-list=localhost,127.0.0.1,internal-api.example.com",
],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();

Reducing bandwidth through request interception

When scraping data-focused pages, assets such as images, fonts, and stylesheets add unnecessary bandwidth usage without improving the extracted data. Enabling page.setRequestInterception(true) and blocking these resources can significantly reduce proxy traffic and lower costs, especially on paid proxy plans.

// filename: advanced-block-resources.js
const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_URL = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
const BLOCKED_TYPES = new Set(["image", "stylesheet", "font", "media"]);
(async () => {
const anonymizedProxy = await proxyChain.anonymizeProxy(PROXY_URL);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on("request", (req) => {
if (BLOCKED_TYPES.has(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
await page.goto("https://example.com", { waitUntil: "networkidle2" });
console.log("Page loaded — images, CSS, and fonts were blocked.");
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();

Comparing Puppeteer’s approach with Playwright? See Puppeteer vs. Playwright and Playwright web scraping for a detailed breakdown of how they differ.

Choosing the right proxy type for Puppeteer

The proxy type you use often has a bigger impact on success rates than code-level optimizations. Matching the proxy to your target and workload is key to satisfactory performance. 

Residential proxies

IPs sourced from real ISPs and home networks. These closely resemble genuine user traffic, making them the hardest for anti-bot systems to detect or block. To learn more, see Decodo residential proxies and what is a residential proxy? 

  • Best for: heavily protected sites (e.g., Google, Amazon, social platforms), geo-targeted scraping
  • Trade-off: higher cost per GB and slightly higher latency than datacenter proxies 

Datacenter proxies

IPs hosted in cloud or datacenter environments. They offer high-speed, large-scale availability at a lower cost.

  • Best for: high-volume scraping on lightly protected sites, latency-sensitive tasks
  • Trade-off: more easily detected and blocked by advanced anti-bot systems due to known IP ranges 

ISP proxies

Proxies hosted on the datacenter infrastructure but registered with real ISP allocations. They combine the trust signals of residential IPs with the performance of datacenter networks.

  • Best for: sites that block datacenter IPs but require lower latency than residential proxies
  • Trade-off: sits in the middle ground on cost and scalability 

SOCKS5 vs. HTTP

HTTP proxies operate at the application layer and are optimized for standard web traffic, making them sufficient for most scraping use cases.

SOCKS5 proxies operate at a lower level and support any protocol, not just HTTP/HTTPS. They are useful when you need broader protocol support or want an alternative routing method if HTTP proxies are being restricted.

Proxy type

Best for

Speed

Cost

Detection risk

Residential

Protected targets, geo-scraping

Moderate

Higher

Low

Datacenter

High-volume, low-protection targets

Fast

Lower

Higher

ISP

Mixed — trust + speed

Fast

Moderate

Low-moderate

SOCKS5

Non-HTTP traffic, protocol flexibility

Varies

Varies

Varies

Best practices 

Test the proxy before writing any scraping logic 

Run a curl command against your proxy endpoint first. On Windows Command Prompt or macOS Terminal:

curl -x "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000" "https://httpbin.org/ip"

If this returns an IP address, your credentials are valid. If it returns a 407, fix the credentials before touching Puppeteer.

Use headless: "new" when launching

Puppeteer's new headless mode has better stealth characteristics than the legacy mode:

const browser = await puppeteer.launch({ headless: "new" });

Set a realistic user agent and viewport

A blank user agent or an unusual viewport size is a fingerprint in itself:

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.setViewport({ width: 1280, height: 800 });

Add randomized delays between navigations

Human browsing isn't instant. A 2-8 second delay between requests significantly reduces detection risk:

const delay = (ms) => new Promise((res) => setTimeout(res, ms));
await delay(Math.floor(Math.random() * 6000) + 2000);

Store credentials in environment variables

Never hardcode proxy credentials in source files. This is how you should do it:

const PROXY_URL = `http://${process.env.PROXY_USER}:${process.env.PROXY_PASS}@gate.decodo.com:10000`;

Set them in your terminal before running.

Windows (Command Prompt):

set PROXY_USER=your_username
set PROXY_PASS=your_password

macOS:

export PROXY_USER=your_username
export PROXY_PASS=your_password

Common mistakes

Here are some common pitfalls to avoid:

  • Passing credentials inline in --proxy-server. Chrome silently ignores user:pass@host:port in this flag. No error is thrown — requests just fail with 407. Always use page.authenticate() or proxy-chain.
  • Installing proxy-chain v3.x. Version 3 dropped CommonJS support. require("proxy-chain") will throw ERR_REQUIRE_ESM. Install v2.x explicitly: npm install proxy-chain@2.
  • Forgetting to call page.authenticate() before page.goto(). The order is to authenticate first, navigate second. Reversing the order means the first request goes out unauthenticated.
  • Not closing anonymized proxies. Every anonymizeProxy() call opens a local server on a random port. Always call closeAnonymizedProxy() in a finally block to prevent port leaks.
  • Using free public proxies for real work. Free proxies are slow, unreliable, and already banned on most protected sites. Use them only for smoke testing that the code runs, not for validating that the scraping logic works.
  • Reusing the same IP too rapidly. Even with residential proxies, hitting a target 50 times per second from one IP will get that IP flagged. Rotate more frequently and add delays.

If you're dealing with anti-bot defenses, it's worth understanding the broader techniques used to avoid detection. For a deeper dive, see Navigating Anti-Bot SystemsWeb Scraping Without Getting Blocked, and How to Bypass CAPTCHA with Puppeteer.

Troubleshooting proxy issues in Puppeteer

ERR_NO_SUPPORTED_PROXIES

Cause: The proxy string format is wrong, the protocol prefix doesn't match what the provider expects, or credentials aren't being passed through correctly.

Fix: Confirm the format with curl first. If curl works but Puppeteer doesn't, you're likely missing the proxy-chain wrapping; Chrome can't handle inline credentials.

ERR_INVALID_AUTH_CREDENTIALS

Cause: The proxy received the request but rejected the credentials. It could be a wrong username, a wrong password, or the proxy plan isn't active.

Fix: Re-copy credentials fresh from your provider dashboard. Watch for leading/trailing spaces and special characters. Test with curl independently before going back to the Puppeteer code.

407 Proxy Authentication Required

Cause: Either no credentials were provided, or the wrong authentication method was used.

Fix: Switch from the inline URL format to page.authenticate() or proxy-chain. Double-check that page.authenticate() is called before page.goto().

503 Service Temporarily Unavailable

Cause. The specific exit node the proxy assigned is temporarily overloaded. This is a provider-side issue, not a code problem.

Fix: Run the script again. Rotating proxy endpoints assign a different exit node on each connection, so a second attempt will almost always succeed.

Pages are loading slowly or timing out

Cause: The proxy endpoint is geographically distant or overloaded. Default Puppeteer timeouts (30 seconds) may not be enough.

Fix: Increase the timeout and test proxy latency independently:

await page.goto("https://httpbin.org/ip", {
waitUntil: "networkidle2",
timeout: 60000, // 60 seconds
});

CAPTCHA walls are appearing despite proxies

Cause: The proxy IP has been flagged, or the browser fingerprint is identifiable as automated traffic.

Fix: Rotate to a fresh IP, set a realistic user agent and viewport, and add delays. If you're using datacenter proxies, switch to residential — they're significantly harder to fingerprint. See how to bypass CAPTCHAs.

Implementing retry logic with exponential backoff

Wrap your scraping logic in a retry loop. On failure, switch to a new proxy and wait progressively longer between attempts:

// filename: troubleshoot-retry.js
const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXIES = [
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10001",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10002",
];
const MAX_RETRIES = 3;
async function scrapeWithRetry(url) {
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const proxyUrl = PROXIES[attempt % PROXIES.length];
const anonymizedProxy = await proxyChain.anonymizeProxy(proxyUrl);
console.log(`Attempt ${attempt + 1} — Anonymized proxy: ${anonymizedProxy}`);
let browser;
try {
browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
page.on("requestfailed", (req) => {
console.warn(`Request failed: ${req.url()} — ${req.failure().errorText}`);
});
await page.goto(url, { waitUntil: "networkidle2", timeout: 30000 });
const body = await page.$eval("body", (el) => el.innerText);
console.log(`Success on attempt ${attempt + 1}:`, body);
return body;
} catch (err) {
console.error(`Attempt ${attempt + 1} failed: ${err.message}`);
// Exponential backoff: 2s, 4s, 8s
const delay = Math.pow(2, attempt + 1) * 1000;
console.log(`Retrying in ${delay / 1000}s...`);
await new Promise((res) => setTimeout(res, delay));
} finally {
if (browser) await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
}
}
throw new Error(`All ${MAX_RETRIES} attempts failed.`);
}
(async () => {
try {
await scrapeWithRetry("https://httpbin.org/ip");
} catch (err) {
console.error("Scraping failed:", err.message);
}
})();

See retry logic for more on the pattern.

Debugging tips

1. Launch in headful mode to see exactly what Chromium is doing:

const browser = await puppeteer.launch({ headless: false });

2. Listen to request failures to catch proxy-related issues at the network level:

page.on("requestfailed", (req) => {
console.log(req.url(), req.failure().errorText);
});

3. Test without a proxy first. If the issue reproduces without a proxy, it's your scraping logic, not the proxy. Isolating this saves a lot of debugging time.

Full code reference

All scripts used in this guide are ready to copy and run. Replace YOUR_USERNAME and YOUR_PASSWORD with your Decodo credentials throughout.

verify.js

const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "verify.png" });
console.log("Screenshot saved. Setup is working.");
await browser.close();
})();

static-proxy.js

const puppeteer = require("puppeteer");
// Works with IP whitelisting auth only — Chrome ignores inline credentials
const PROXY = "http://gate.decodo.com:10001";
(async () => {
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${PROXY}`],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
})();

auth-page.js

const puppeteer = require("puppeteer");
const PROXY_HOST = "gate.decodo.com";
const PROXY_PORT = "10001";
const PROXY_USER = "YOUR_USERNAME";
const PROXY_PASS = "YOUR_PASSWORD";
(async () => {
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("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
})();

auth-proxychain.js

const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_URL = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10001";
(async () => {
const anonymizedProxy = await proxyChain.anonymizeProxy(PROXY_URL);
console.log("Local proxy address:", anonymizedProxy);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();

auth-header.js

// Last-resort fallback — only works for HTTP targets, not HTTPS
const puppeteer = require("puppeteer");
const PROXY_HOST = "gate.decodo.com";
const PROXY_PORT = "10000";
const PROXY_USER = "YOUR_USERNAME";
const PROXY_PASS = "YOUR_PASSWORD";
const credentials = Buffer.from(`${PROXY_USER}:${PROXY_PASS}`).toString("base64");
(async () => {
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=http://${PROXY_HOST}:${PROXY_PORT}`],
});
const page = await browser.newPage();
await page.setExtraHTTPHeaders({
"Proxy-Authorization": `Basic ${credentials}`,
});
await page.goto("http://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
})();

rotate-random.js

const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXIES = [
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10001",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10002",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10003",
];
const URLS_TO_SCRAPE = [
"https://httpbin.org/ip",
"https://httpbin.org/ip",
"https://httpbin.org/ip",
];
function getRandomProxy() {
return PROXIES[Math.floor(Math.random() * PROXIES.length)];
}
(async () => {
for (const url of URLS_TO_SCRAPE) {
const proxyUrl = getRandomProxy();
const anonymizedProxy = await proxyChain.anonymizeProxy(proxyUrl);
console.log(`Anonymized proxy: ${anonymizedProxy}`);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("Response:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
}
})();

rotate-roundrobin.js

const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXIES = [
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10001",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10002",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10003",
];
const URLS_TO_SCRAPE = [
"https://httpbin.org/ip",
"https://httpbin.org/ip",
"https://httpbin.org/ip",
];
(async () => {
for (let i = 0; i < URLS_TO_SCRAPE.length; i++) {
const proxyUrl = PROXIES[i % PROXIES.length];
const anonymizedProxy = await proxyChain.anonymizeProxy(proxyUrl);
console.log(`Request ${i + 1} — Anonymized proxy: ${anonymizedProxy}`);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.goto(URLS_TO_SCRAPE[i], { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("Response:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
}
})();

rotate-gateway.js

const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_URL = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
(async () => {
const anonymizedProxy = await proxyChain.anonymizeProxy(PROXY_URL);
console.log("Anonymized proxy:", anonymizedProxy);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP on request 1:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();

advanced-per-page.js

const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_1 = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10001";
const PROXY_2 = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10002";
(async () => {
const anonymizedProxy1 = await proxyChain.anonymizeProxy(PROXY_1);
const anonymizedProxy2 = await proxyChain.anonymizeProxy(PROXY_2);
console.log("Proxy 1:", anonymizedProxy1);
console.log("Proxy 2:", anonymizedProxy2);
const [browser1, browser2] = await Promise.all([
puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy1}`],
}),
puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy2}`],
}),
]);
const [page1, page2] = await Promise.all([
browser1.newPage(),
browser2.newPage(),
]);
await Promise.all([
page1.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" }),
page2.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" }),
]);
const body1 = await page1.$eval("body", (el) => el.innerText);
const body2 = await page2.$eval("body", (el) => el.innerText);
console.log("Page 1 IP:", body1);
console.log("Page 2 IP:", body2);
await Promise.all([browser1.close(), browser2.close()]);
await Promise.all([
proxyChain.closeAnonymizedProxy(anonymizedProxy1, true),
proxyChain.closeAnonymizedProxy(anonymizedProxy2, true),
]);
})();

advanced-chaining.js

const puppeteer = require("puppeteer");
const ProxyChain = require("proxy-chain");
const UPSTREAM_PROXY = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
(async () => {
const server = new ProxyChain.Server({
port: 8099,
prepareRequestFunction: () => {
return { upstreamProxyUrl: UPSTREAM_PROXY };
},
});
await new Promise((resolve) => server.listen(resolve));
console.log("Local proxy server running on port 8099");
const browser = await puppeteer.launch({
headless: "new",
args: ["--proxy-server=http://127.0.0.1:8099"],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
await new Promise((resolve) => server.close(resolve));
})();

advanced-bypass.js

const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_URL = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
(async () => {
const anonymizedProxy = await proxyChain.anonymizeProxy(PROXY_URL);
console.log("Anonymized proxy:", anonymizedProxy);
const browser = await puppeteer.launch({
headless: "new",
args: [
`--proxy-server=${anonymizedProxy}`,
"--proxy-bypass-list=localhost,127.0.0.1,internal-api.example.com",
],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip", { waitUntil: "networkidle2" });
const body = await page.$eval("body", (el) => el.innerText);
console.log("IP seen by target site:", body);
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();

advanced-block-resources.js

const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXY_URL = "http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10000";
const BLOCKED_TYPES = new Set(["image", "stylesheet", "font", "media"]);
(async () => {
const anonymizedProxy = await proxyChain.anonymizeProxy(PROXY_URL);
console.log("Anonymized proxy:", anonymizedProxy);
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on("request", (req) => {
if (BLOCKED_TYPES.has(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
await page.goto("https://example.com", { waitUntil: "networkidle2" });
console.log("Page loaded — images, CSS, and fonts were blocked.");
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();

troubleshoot-retry.js

const puppeteer = require("puppeteer");
const proxyChain = require("proxy-chain");
const PROXIES = [
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10001",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10002",
"http://YOUR_USERNAME:YOUR_PASSWORD@gate.decodo.com:10003",
];
const MAX_RETRIES = 3;
async function scrapeWithRetry(url) {
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const proxyUrl = PROXIES[attempt % PROXIES.length];
const anonymizedProxy = await proxyChain.anonymizeProxy(proxyUrl);
console.log(`Attempt ${attempt + 1} — Anonymized proxy: ${anonymizedProxy}`);
let browser;
try {
browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${anonymizedProxy}`],
});
const page = await browser.newPage();
page.on("requestfailed", (req) => {
console.warn(`Request failed: ${req.url()} — ${req.failure().errorText}`);
});
await page.goto(url, { waitUntil: "networkidle2", timeout: 30000 });
const body = await page.$eval("body", (el) => el.innerText);
console.log(`Success on attempt ${attempt + 1}:`, body);
return body;
} catch (err) {
console.error(`Attempt ${attempt + 1} failed: ${err.message}`);
const delay = Math.pow(2, attempt + 1) * 1000;
console.log(`Retrying in ${delay / 1000}s...`);
await new Promise((res) => setTimeout(res, delay));
} finally {
if (browser) await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
}
}
throw new Error(`All ${MAX_RETRIES} attempts failed.`);
}
(async () => {
try {
await scrapeWithRetry("https://httpbin.org/ip");
} catch (err) {
console.error("Scraping failed:", err.message);
}
})();

Final thoughts

This guide covered the full spectrum of proxy configuration in Puppeteer, from the basic --proxy-server setup to authentication, rotation strategies, advanced per-page routing, and production-ready retry handling.

A few things worth keeping in mind as you ship:

  • proxy-chain v2.x is the most reliable authentication method, so use it as your default
  • Always verify credentials with curl before writing any Puppeteer code
  • Gateway rotation endpoints handle IP cycling server-side and are the cleanest setup for production scraping
  • Residential proxies are the right choice for protected targets; the extra cost is worth it when the alternative is a blocked IP

Our residential proxies are compatible with all the methods covered in this guide out of the box. The rotating endpoint supports both username/password authentication and IP whitelisting, geo-targeting enables routing to specific countries or cities, and automatic rotation handles IP cycling without requiring any client-side logic.

Proxy sorted, anti-bot not?

Decodo's Web Scraping API handles proxy rotation, CAPTCHA solving, and fingerprint evasion so your Puppeteer scripts don't need stealth plugins and workarounds.

Share article:

About the author

Vilius Sakutis

Head of Partnerships

Vilius leads performance marketing initiatives with expertize rooted in affiliates and SaaS marketing strategies. Armed with a Master's in International Marketing and Management, he combines academic insight with hands-on experience to drive measurable results in digital marketing campaigns.

Connect with Vilius via LinkedIn

All information on Decodo Blog is provided on an as is basis and for informational purposes only. We make no representation and disclaim all liability with respect to your use of any information contained on Decodo Blog or any third-party websites that may belinked therein.

Frequently asked questions

Does Puppeteer have built-in proxy support?

Yes, partially. Puppeteer supports passing a proxy server via the --proxy-server launch flag. For authentication, it also has the built-in page.authenticate() method. What it doesn't support natively is inline credentials in the proxy URL — Chrome silently ignores them, which is why the proxy-chain package is commonly used alongside Puppeteer.

How do I add a proxy to Puppeteer?

Pass the proxy address in the args array of puppeteer.launch(). For authenticated proxies, wrap the credentials with proxy-chain and pass the local address to --proxy-server instead.

Is it legal to use a proxy server?

The legal considerations typically center on what you do with the proxy; scraping publicly available data is generally accepted, while circumventing paywalls or violating a site's terms of service can create legal exposure. Using ethically sourced proxies from a reputable provider is the responsible baseline. Consult a legal professional if you have specific concerns about your use case.

What is the best proxy type for Puppeteer web scraping?

Residential proxies for protected targets: Google, Amazon, social media, and any site with serious anti-bot measures. Datacenter proxies for high-volume scraping on lightly protected targets where speed matters more than stealth. ISP proxies when you need both speed and residential-level trust signals.

Notice document showing lines of text over dark gradient UI with colorful code-like bars and a progress bar below

Puppeteer vs. Selenium: Which Tool Should You Use for Web Scraping?

Puppeteer and Selenium are the 2 most-used browser automation tools for scraping JavaScript-heavy pages. Comparing puppeteer vs selenium for web scraping isn't just about speed. Browser support, language support, and anti-bot handling all play a role. This guide covers what each tool does well, what it doesn't, and how to pick the right one.

Puppeteer vs. Playwright: Which Tool Is Better for Web Scraping?

Puppeteer vs. Playwright is a real architectural decision for any production scraping project. The two libraries share a common origin: Playwright was built at Microsoft by engineers who previously worked on Puppeteer at Google. Yet they're different on browser coverage, language bindings, and scraping ergonomics. Performance, stealth, proxy integration, and parallel execution decide which tool fits your pipeline.

Browser window labeled Puppeteer suspended by a marionette cross on a dark background

How to Bypass CAPTCHA With Puppeteer: A Step-By-Step Guide

Since their inception in 2000, CAPTCHAs have been crucial for website security, distinguishing human users from bots. They are a savior for website owners and a nightmare for data gatherers. While CAPTCHAs enhance website integrity, they pose challenges for those reliant on automated data gathering. In this comprehensive guide, we delve into the fundamentals of Puppeteer, focusing on techniques for CAPTCHA detection and avoidance using Puppeteer. We also explore strategies for how to bypass CAPTCHA verification, methods for solving CAPTCHAs with specialized third-party services, and the alternative solutions provided by our Site Unblocker.

© 2018-2026 decodo.com (formerly smartproxy.com). All Rights Reserved