

Puppeteer drives a real headless Chrome instance, handles JavaScript rendering, manages sessions, and automates browser interactions that lightweight HTTP clients can't touch.
The catch is scale. Point it at a real target from a single IP and rate limits kick in fast. Rotating residential proxies fix this by cycling through a pool of real residential IPs, so each session looks like a distinct organic user.
This guide is for Node.js developers who want a reliable, production-ready proxy rotation setup with Puppeteer.
Puppeteer is the Node.js library that controls headless Chrome. It bundles a compatible Chromium binary, so there's no separate browser setup.
For proxy authentication, the proxy-chain package (from the apify/proxy-chain project) is the most reliable option. Puppeteer's --proxy-server flag sets the proxy at launch, but Chrome ignores credentials embedded in the proxy URL. proxy-chain solves this by running a local proxy server that holds your credentials and forwards traffic upstream, which removes the need to call page.authenticate() on every page.
To reduce headless detection, puppeteer-extra combined with puppeteer-extra-plugin-stealth is the standard open-source approach. The stealth plugin patches fingerprinting vectors that Chromium exposes by default, including navigator.webdriver, missing browser plugins, and inconsistent WebGL values.
Finally, you need a rotating residential proxy provider with a gateway endpoint that cycles IPs automatically.
If you're starting a new project and have no Chrome-specific requirement, Playwright is a strong alternative with built-in per-context proxy support. For existing Puppeteer codebases, there's no compelling reason to migrate.
Initialize a Node.js project and install the core stack: Puppeteer, proxy-chain for authentication, and puppeteer-extra with the stealth plugin.
bash
npm install puppeteer proxy-chain puppeteer-extra puppeteer-extra-plugin-stealth
That gives you Puppeteer with its bundled Chromium binary, the proxy-chain library, and the puppeteer-extra plugin system with stealth evasions.
Puppeteer exposes Chrome's --proxy-server flag through the args option in puppeteer.launch(). This sets the proxy for the entire browser instance, so every page routes through the same endpoint.
javascript
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=http://${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`]
});
const page = await browser.newPage();
await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
})();
If you're running in Docker or CI, add '--no-sandbox' and '--disable-dev-shm-usage' to the args array. Missing these is the most common cause of a launch failure that looks like a proxy problem.
Most residential proxy providers require username and password authentication. Chrome ignores credentials in the proxy URL, so you call page.authenticate() on each new page – and it has to javascript
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=http://${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`]
});
const page = await browser.newPage();
// Authenticate before any navigation
await page.authenticate({
username: process.env.PROXY_USER,
password: process.env.PROXY_PASS
});
await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
})();
Get your credentials from the GoProxies dashboard and drop them straight into your environment.
The proxy-chain library wraps your authenticated proxy URL into a local endpoint, so Puppeteer connects locally and credentials are injected transparently. No more page.authenticate() on every page.
anonymizeProxy takes your full proxy URL and returns a local endpoint to pass to --proxy-server:
javascript
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const proxyChain = require('proxy-chain');
puppeteer.use(StealthPlugin());
(async () => {
const originalProxy = `http://${process.env.PROXY_USER}:${process.env.PROXY_PASS}@${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`;
const anonymizedProxy = await proxyChain.anonymizeProxy(originalProxy);
const browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=${anonymizedProxy}`]
});
const page = await browser.newPage();
await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
})();
Always call closeAnonymizedProxy after closing the browser, or you'll leak local ports across a long run.
Because --proxy-server is fixed for the lifetime of a browser instance, rotating to a fresh IP means launching a new browser per session. The pattern below cycles through a proxy list, spawning a fresh browser for each URL and closing it when done:
javascript
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const proxyChain = require('proxy-chain');
puppeteer.use(StealthPlugin());
const proxies = [
`http://${process.env.PROXY_USER}:${process.env.PROXY_PASS}@HOST1:PORT`,
`http://${process.env.PROXY_USER}:${process.env.PROXY_PASS}@HOST2:PORT`,
`http://${process.env.PROXY_USER}:${process.env.PROXY_PASS}@HOST3:PORT`
];
const urls = [
'https://example.com/page-1',
'https://example.com/page-2',
'https://example.com/page-3'
];
async function scrapeWithProxy(url, proxyUrl) {
const anonymizedProxy = await proxyChain.anonymizeProxy(proxyUrl);
const browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=${anonymizedProxy}`]
});
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
const content = await page.content();
console.log(`Scraped ${url}: ${content.length} bytes`);
} finally {
await browser.close();
await proxyChain.closeAnonymizedProxy(anonymizedProxy, true);
}
}
(async () => {
for (let i = 0; i < urls.length; i++) {
const proxy = proxies[i % proxies.length];
await scrapeWithProxy(urls[i], proxy);
}
})();
If you're using a rotating residential gateway instead of a static list, a single endpoint is enough. The provider assigns a fresh IP on each new connection, so launching a new browser per URL achieves rotation on its own.
Before hitting real targets, confirm the proxy is returning a residential IP. A request to an IP-check endpoint returns your exit IP as plain JSON:
javascript
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=http://${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`]
});
const page = await browser.newPage();
await page.authenticate({
username: process.env.PROXY_USER,
password: process.env.PROXY_PASS
});
await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
const result = await page.evaluate(() => JSON.parse(document.body.innerText));
console.log('Exit IP:', result.origin);
await browser.close();
})();
If the returned IP matches your own machine, authentication failed. Check that page.authenticate() was called before page.goto() and that your credentials are correct.
Proxy quality is the single biggest factor in scraping success. Datacenter IPs get flagged more often on protected targets because their ranges are well known and their fraud scores tend to run high. Residential IPs assigned by real ISPs to real households are far harder for anti-bot systems to single out.
GoProxies offers 30 million residential IPs across 200+ locations with 99.99% uptime and pay-as-you-go pricing. Check the rotating residential proxy plans to find the right fit for your volume. [CONFIRM: pool size, location count, uptime figure]
Some targets use session continuity as a detection signal. Rotating to a new IP mid-session on a site that expects the same IP can trigger a block. Use sticky sessions for workflows that need state across pages – logging in and navigating a funnel, for instance – and reserve rotating IPs for independent, stateless requests.
Headless Chrome exposes properties that headful browsers don't: navigator.webdriver is set to true, plugin lists are empty, and certain WebGL and canvas fingerprints differ from real browsers. puppeteer-extra-plugin-stealth patches these automatically and should be applied on every project.
Rotating IPs without varying anything else still leaves a fingerprint. Set a realistic user agent with page.setUserAgent(), match the Accept-Language header to the proxy's geographic location, and add randomized delays between requests. Uniform timing is one of the clearest bot signals a site can read.
Wrap every page.goto() in a try-catch. On a timeout or connection error, retry with the next proxy rather than letting the script crash. Log failures so you can see which proxies are underperforming.
A browser pool launches a fixed number of concurrent instances, each with its own proxy, and distributes URLs across them. Keep concurrency moderate: too many Chrome instances in parallel will exhaust server memory long before anti-bot systems become your bottleneck.
Puppeteer is detectable by default. Chromium's headless mode leaves signals that anti-bot systems actively check for, and adding a proxy only covers the IP. Everything else needs deliberate configuration.
Sites running fingerprinting checks look for properties that differ between headless Chromium and a real browser: navigator.webdriver set to true, an empty plugins array, inconsistent screen dimensions. These persist regardless of which proxy you use. The stealth plugin patches most of them, but complete evasion isn't guaranteed on the most aggressively protected targets.
A CAPTCHA on the first request usually traces back to the browser rather than the proxy, which is the opposite of what most people assume. Check in this order: whether the stealth plugin is actually applied and navigator.webdriver is no longer exposed, then whether your Accept-Language header matches the proxy's geography, then the exit IP's fraud score.
The reason for that order is that Puppeteer's fingerprint is the loudest signal in the stack. An IP with a perfect reputation won't save a browser that announces itself as automated.
Even residential IPs can carry poor reputation if they've been used heavily for scraping. If you're blocked on the first request, check the exit IP's fraud score before concluding that the target blocks all residential traffic.
Rotating too quickly on session-aware targets can also cause blocks, since no organic user switches IPs every few seconds.
The most common setup mistake is calling page.authenticate() after page.goto(). Chrome sends the proxy authentication challenge during the connection handshake, so credentials have to arrive before navigation starts. proxy-chain's anonymizeProxy eliminates the timing issue entirely.
Puppeteer's advantage over lightweight HTTP clients is JavaScript execution. Single-page applications load their content through client-side API calls after the initial HTML arrives. A standard HTTP request captures that empty shell; Puppeteer waits for the full DOM and captures what a user would actually see. Dynamic product listings, infinite scroll feeds, and client-side search results all fall into this category.
Geo-restricted content is another strong fit. Routing traffic through a residential IP in a specific country, state, or city makes localized pricing, region-specific search results, and location-gated content reachable from any server. Authenticated content, paginated flows, multi-step form submissions, and pages that need a scroll before revealing data are all natural fits too.
GoProxies' 30 million residential IPs across 200+ locations give you the geographic coverage to collect this at scale. Start scraping with GoProxies and reach any target anywhere.
Puppeteer is a browser automation library and is legally neutral on its own. Legality depends on what you collect and how.
Publicly accessible data – content any visitor can see without logging in – is generally treated differently from account-restricted data. Collecting public data for research, price monitoring, or competitive analysis is widely practiced and broadly accepted. Accessing data behind authentication without authorization is a different matter and carries significantly higher legal risk.
Most sites' terms of service prohibit automated access in some form. Violating a ToS isn't a criminal act in most jurisdictions, but it can result in account termination and civil claims. Read the ToS of any site you plan to scrape.
Ethical practice also means respecting rate limits, honoring robots.txt, and avoiding unnecessary server load. Those habits reduce your legal exposure and make your scraping more sustainable. If your use case is commercial and the target is high-profile, get qualified legal advice before running at scale.
Puppeteer launches a full Chrome instance per browser session, which carries a real CPU and memory cost. If the target delivers its content in the initial HTML response without requiring JavaScript, a lightweight HTTP client is faster, cheaper, and simpler.
The call is straightforward: use Puppeteer when the target requires JavaScript rendering, user interaction, or browser-level session management. Use a lightweight HTTP client when the raw HTML response has what you need. Many stacks use both, with the HTTP client handling the bulk of requests and Puppeteer reserved for pages that genuinely need a full browser.
Technically yes. Free proxy lists exist and can be passed to --proxy-server. In practice they create more problems than they solve.
Free proxies are shared across many users, so they accumulate blocks and high fraud scores fast. The IP that works on your first request may be blocklisted by the second. They also lack authentication, offer no uptime guarantees, and go offline without notice. Keeping a working pool of free proxies takes constant maintenance that eats the time you'd spend on actual scraping logic.
For anything beyond initial testing, a reliable rotating residential plan pays for itself in reliability alone. GoProxies offers pay-as-you-go pricing with no minimums or contracts. See the rotating residential proxy pricing to get started.
Using rotating proxies with Puppeteer is straightforward once you understand the core constraint: the proxy is fixed at browser launch, so rotation happens at the browser level.
The setup comes down to three things working together: the --proxy-server flag or proxy-chain for authentication, the stealth plugin for fingerprint evasion, and a quality rotating residential gateway for IP diversity.
The difficulty is moderate for developers comfortable with Node.js and async/await. Use proxy-chain to handle credentials cleanly, apply the stealth plugin on every project, match Accept-Language headers to the proxy's geography, and verify your exit IP before running at scale.
The rich text element allows you to create and format headings, paragraphs, blockquotes, images, and video all in one place instead of having to add and format them individually. Just double-click and easily create content.
A rich text element can be used with static or dynamic content. For static content, just drop it into any page and begin editing. For dynamic content, add a rich text field to any collection and then connect a rich text element to that field in the settings panel. Voila!
Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.
No. Puppeteer doesn't support username and password authentication over SOCKS5. Chromium handles SOCKS5 connections but drops credentials passed in the URL. For authenticated residential proxies, use the HTTP or HTTPS endpoint your provider supplies and authenticate via page.authenticate() or proxy-chain.
Rotating sessions assign a fresh IP on each new browser instance, which suits stateless requests where each page load is independent. Sticky sessions hold the same IP for a set duration, which is necessary for workflows that need session continuity, such as logging in and navigating a multi-step flow. Most providers let you switch between modes via a parameter in the proxy username.
No. The --proxy-server flag and page.authenticate() work with standard Puppeteer. puppeteer-extra is only needed for the stealth plugin and other detection evasion features, which are strongly recommended for targets that actively fingerprint headless browsers.
Your limit is available memory. Each Chrome instance typically consumes a few hundred megabytes depending on the pages it loads, so a small server will hit memory pressure sooner than you'd expect. Start conservative, measure actual usage on your own hardware, and scale from there.