Puppeteer mobile proxy setup
Puppeteer passes the proxy to Chrome as a launch flag, and Chrome will not accept credentials in that flag — which is why the natural first attempt produces a proxy authentication dialog and a hang rather than a page. The working pattern is two steps: the server in the launch argument, the credentials through page.authenticate. Everything else follows from there.
The problem: --proxy-server does not take a password
Chrome's --proxy-server flag accepts a scheme, host and port, and nothing else. Put credentials in it and Chrome ignores them, the upstream answers 407, and Puppeteer sits at a modal auth prompt you cannot see in headless mode — so the symptom is a navigation that never resolves rather than an error that names the cause. Underneath that, the same thing that pushes people to Puppeteer in the first place still applies: a headless Chrome running from a cloud IP is trivially identifiable, so the content you are automating against may not be the content a real visitor gets.
You will recognise this if
- goto() hangs until timeout with no error after adding --proxy-server with credentials.
- ERR_INVALID_AUTH_CREDENTIALS or a silent 407 in the network log.
- Auth that works on the first page and fails on every subsequent one.
- New pages or popups bypassing the proxy auth you set on the first page.
- Bandwidth cost scaling with full page weight because nothing is intercepted.
Working code
Every block below runs as written once PROXY_URL holds a real connection string. Copy yours from the dashboard after your first top-up.
Set the connection URL
bashexport PROXY_URL="http://USERNAME-mbl-us-sid-pptr01-rot-sticky:[email protected]:7000"
# One browser instance per session. Chrome applies the proxy flag
# process-wide, so a second identity means a second browser.The working pattern: flag for host, authenticate for credentials
javascriptimport puppeteer from 'puppeteer';
const url = new URL(process.env.PROXY_URL);
const browser = await puppeteer.launch({
headless: 'new',
args: [
// Host and port ONLY — Chrome ignores credentials placed here.
`--proxy-server=http://${url.host}`,
],
});
const page = await browser.newPage();
// This is the step that is missing from most examples.
await page.authenticate({
username: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
});
await page.goto('https://api.ipify.org?format=json', { waitUntil: 'domcontentloaded' });
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();page.authenticate is per page. Every new page or popup needs its own call — see below.
Authenticate every page, including popups
javascriptconst creds = {
username: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
};
// Catch pages the site opens itself, which otherwise hit a 407 you never see.
browser.on('targetcreated', async (target) => {
if (target.type() !== 'page') return;
const p = await target.page();
if (p) await p.authenticate(creds);
});
const page = await browser.newPage();
await page.authenticate(creds);Without the targetcreated hook, a window.open on the target silently breaks the run.
Intercept requests — you pay per gigabyte
javascriptawait page.setRequestInterception(true);
page.on('request', (req) => {
if (['image', 'media', 'font', 'stylesheet'].includes(req.resourceType())) {
return req.abort();
}
return req.continue();
});
// Mobile viewport, so the site serves its mobile build to your mobile IP.
await page.setViewport({ width: 390, height: 844, isMobile: true, deviceScaleFactor: 3 });
await page.goto('https://target.example/product/123', { waitUntil: 'domcontentloaded' });Interception must be enabled before goto, and every request needs exactly one abort or continue.
Which rotation mode to use
-rot-stickyChrome takes the proxy as a process-level launch flag, so the whole browser shares one exit — the unit of identity in Puppeteer is the browser instance, not the page. That makes sticky the natural fit: pin the browser with -rot-sticky and a distinct -sid-, run the session to completion, then close it. When you want a different identity, launch a second browser with a different sid rather than trying to move the exit under a live Chrome, which will invalidate whatever session state you had built up.
When to choose something else
| Mode | Use it when |
|---|---|
| auto10 | Short-lived browsers doing stateless work, where each launch handles a handful of independent pages. |
| hard | Long monitoring sessions that must not change device for hours at a time. |
| auto30 | Background crawls that want some IP diversity across a long run without disturbing short sessions. |
Expected bandwidth use
Puppeteer drives real Chrome, so it downloads what Chrome downloads. Request interception is not an optimisation here — on a per-GB pool it is the main cost control you have.
| Workload | Per unit | Per GB |
|---|---|---|
| Typical page, all assets, fresh profile | 2–5 MB | ~200–520 pages |
| Same page, images/media/fonts aborted | 400–900 KB | ~1,150–2,600 pages |
| Same page, reused browser with warm cache | 150–500 KB | ~2,000–7,000 pages |
| Document blocked, XHR/fetch only | 5–50 KB | ~20,000–200,000 calls |
Launching a fresh browser per URL throws away the cache and re-downloads every asset — it is the most common reason a Puppeteer bill lands several times over estimate. Reuse one browser across the pages in a session, intercept aggressively, and only take screenshots on the pages that actually fail. Ranges are typical for commercial sites; meter your own before sizing a budget.
| Bandwidth | Price | Per GB | Save |
|---|---|---|---|
| 1 GB | $7 | $7 | — |
| 5 GB | $35 | $7 | — |
| 10 GB | $68 | $6.80 | 3% |
| 25 GB | $150 | $6 | 14% |
| 50 GB | $283 | $5.66 | 19% |
| 100 GB | $500 | $5 | 29% |
Relevant countries
Change country by editing one token in the proxy username — no redeploy, no second account. Pool depth varies by country and by whether you use the mobile (mbl) or residential (peer) pool; live availability is shown on each country page and in your dashboard.
Legitimate use only
ProxyMobile is sold for measurement, quality assurance and research on systems you own or are authorised to test. Buying bandwidth does not grant permission to access a third party's systems. You remain responsible for complying with each target's terms of service, robots directives and rate limits, and with the data-protection law that applies to you.
For this use case
- Automate sites you own or are authorised to automate against.
- A headless browser generates load far faster than a human — throttle deliberately and honour Retry-After.
- Do not use automation to bulk-create accounts, defeat captchas you were not meant to pass, or bypass access controls.
- Keep synthetic traffic out of other people's analytics where you reasonably can, and never generate billable events you do not intend.
Never, on any plan
- No credential stuffing, account takeover, or any attempt to authenticate as someone else.
- No circumventing an access control, paywall or security measure you have not been authorised to test.
- No collection of personal data you have no lawful basis to process.
- No load that a reasonable operator would experience as a denial of service — throttle, back off on 429s, and honour Retry-After.
Accounts used for fraud, intrusion or bulk abuse are terminated without refund, and we cooperate with valid legal process. Full terms are in our acceptable use policy.
Puppeteer FAQ
Why does --proxy-server with credentials in the URL not work?
Chrome's flag accepts only a scheme, host and port. Credentials placed there are ignored, the upstream returns 407, and headless Chrome waits at an invisible auth dialog — which is why the symptom is a hang rather than an error. Pass the credentials with page.authenticate instead.
Do I need to call page.authenticate on every page?
Yes. It is per page, not per browser. Call it on each page you create, and add a browser.on("targetcreated") hook so pages the site opens itself get authenticated too.
Can I use different proxies for different pages in one browser?
Not with the launch flag — Chrome applies it process-wide, so one browser means one exit. Launch a separate browser instance per identity, each with its own -sid-. If you need per-context proxies in a single browser, Playwright supports that directly.
Sticky or rotating for Puppeteer?
Sticky. The browser is the unit of identity, and it carries cookies and session state for its lifetime, so you want the exit pinned for that lifetime. Launch a new browser with a new sid when you want a new identity.
How do I keep bandwidth down?
Enable setRequestInterception before navigating and abort image, media, font and stylesheet requests, then reuse one browser so the cache stays warm. That typically cuts a run by 70–80%.
Does this work with puppeteer-extra and stealth plugins?
Yes — the proxy configuration is ordinary Puppeteer launch and page API, so plugins that wrap the launcher inherit it. The proxy and the fingerprint are separate concerns, and a carrier IP addresses the one that fingerprint plugins cannot.
Related guides
Try it against your own targets
Bandwidth estimates on this page are honest ranges, not promises — the only number that matters is what your targets cost you. Your first top-up is credited with an extra $2, which is enough to meter a real run before you size a budget.
