Playwright · Chromium, Firefox, WebKit

Playwright mobile proxy setup

Playwright takes a proxy at launch and per browser context, which makes it the easiest of the automation frameworks to point at a mobile pool — once you know that credentials go in the proxy object rather than in the server URL. This page covers the setup that actually works, how to map sessions onto contexts, and how to keep a browser-driven workload from quietly costing you a fortune in bandwidth.

Start with $2 free creditSee pricingNo subscription · pay per GB

The problem: cloud browsers get walled, and proxy auth is fiddly

Two things bite in order. First, a headless browser running from CI or a cloud VM is transparently identifiable by IP alone — the entire range is known — so bot management engages before your selectors ever run, and you spend days tuning fingerprints that were never the problem. Second, when you do reach for a proxy, the obvious move of embedding credentials in the server URL does not work: Playwright expects them as separate username and password fields, and a malformed attempt fails with a connection error that says nothing useful about why.

You will recognise this if

  • Selectors that work locally and time out in CI, against the same URL.
  • net::ERR_TUNNEL_CONNECTION_FAILED or a hang at goto() after adding a proxy.
  • Challenge pages, interstitials or empty shells instead of content.
  • Bandwidth bills that scale with page weight because every asset loads.
  • Parallel workers interfering with each other because they share one exit.

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

bash
export PROXY_URL="http://USERNAME-mbl-us-sid-pw01-rot-sticky:[email protected]:7000"

# One -sid- per browser context. Parallel workers need distinct sids.

Launch-level proxy (the correct auth shape)

javascript
import { chromium } from 'playwright';

const url = new URL(process.env.PROXY_URL);

const browser = await chromium.launch({
  proxy: {
    // Host and port ONLY. Credentials do not go in this string.
    server: `http://${url.host}`,
    username: decodeURIComponent(url.username),
    password: decodeURIComponent(url.password),
  },
});

const page = await browser.newPage();
await page.goto('https://api.ipify.org?format=json');
console.log(await page.textContent('body'));   // your carrier exit IP

await browser.close();

decodeURIComponent matters: the username contains hyphenated tokens and the key may be percent-encoded.

Per-context proxies — many sessions, one browser

javascript
import { chromium } from 'playwright';

const base = new URL(process.env.PROXY_URL);

function proxyForSession(sid) {
  return {
    server: `http://${base.host}`,
    username: decodeURIComponent(base.username).replace(/-sid-[a-z0-9_]+/, `-sid-${sid}`),
    password: decodeURIComponent(base.password),
  };
}

// Launch once; give each context its own device via its own sid.
const browser = await chromium.launch();

const sids = ['job-a', 'job-b', 'job-c'];
const contexts = await Promise.all(
  sids.map((sid) => browser.newContext({ proxy: proxyForSession(sid) })),
);

for (const [i, ctx] of contexts.entries()) {
  const page = await ctx.newPage();
  await page.goto('https://api.ipify.org?format=json');
  console.log(sids[i], await page.textContent('body'));
}

await browser.close();

One context per session is the right mental model: contexts isolate cookies and storage, sids isolate devices.

Cut bandwidth by 70–80% with route blocking

javascript
const context = await browser.newContext({ proxy: proxyForSession('job-a') });

// You pay per gigabyte, so do not download what you will not assert on.
await context.route('**/*', (route) => {
  const type = route.request().resourceType();
  if (['image', 'media', 'font', 'stylesheet'].includes(type)) {
    return route.abort();
  }
  return route.continue();
});

const page = await context.newPage();
await page.goto('https://target.example/product/123', { waitUntil: 'domcontentloaded' });
console.log(await page.textContent('[data-testid="price"]'));

Drop stylesheet from the list if you assert on computed layout or take screenshots.

Which rotation mode to use

Recommended-rot-sticky

A browser context is a session: it carries cookies, local storage and usually a login. If the exit IP changes underneath it, the site sees a session that teleported and will frequently invalidate it — which surfaces as a flaky test or a mid-crawl logout rather than as an obvious proxy error. Pin each context with -rot-sticky and give it its own -sid-, so context lifetime and IP lifetime line up. When you want a new identity, make a new context with a new sid rather than rotating under the old one.

When to choose something else

ModeUse it when
auto10Stateless crawling where each page is independent and you are not carrying a session — a link-check sweep, say.
hardLong-lived contexts that must not move device at all, such as an hours-long monitoring session.
auto30Background crawls where a slow churn adds IP diversity without disrupting short sessions.

Expected bandwidth use

Playwright downloads everything a real browser downloads, which is the point — and the cost. On a pay-per-GB pool, request interception is the single highest-leverage line of code in your script.

WorkloadPer unitPer GB
Typical page, all assets, cold cache2–5 MB~200–520 pages
Same page, images/media/fonts blocked400–900 KB~1,150–2,600 pages
Same page, warm context cache (repeat visit)150–500 KB~2,000–7,000 pages
API-only interception (block document too)5–50 KB~20,000–200,000 calls

Reuse contexts across navigations so the cache stays warm — launching a fresh browser per page re-downloads every asset and is the most common reason a Playwright bill comes in several times over estimate. Combine a warm context with route blocking and a browser workload lands within a small multiple of plain HTTP fetching. Figures are typical ranges for commercial sites; meter your own targets.

BandwidthPricePer GBSave
1 GB$7$7—
5 GB$35$7—
10 GB$68$6.803%
25 GB$150$614%
50 GB$283$5.6619%
100 GB$500$529%

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 that you are authorised to test or collect from.
  • Respect robots directives and rate limits — a headless browser can generate load far faster than a person, and that is your responsibility to throttle.
  • Do not use automation to create accounts at scale, defeat a captcha you were not meant to pass, or bypass an access control.
  • Identify your automation honestly where a target asks you to, and honour Retry-After when you get it.

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.

Playwright FAQ

Why does my Playwright proxy connection fail with credentials in the URL?

Because Playwright expects them separately. The server field takes host and port only — put the credentials in the username and password fields of the proxy object, and run them through decodeURIComponent first since the connection URL is percent-encoded.

Can I use a different proxy per browser context?

Yes, and it is the recommended pattern. Pass a proxy to newContext() and rewrite the -sid- token per context, so each context gets its own device while you still launch the browser only once.

Sticky or rotating for Playwright?

Sticky, in almost all cases. A context holds cookies and a login, so an IP that rotates underneath it makes the session look like it teleported and gets it invalidated. Create a new context with a new sid when you want a new identity.

How do I keep a browser workload from burning bandwidth?

Block images, media and fonts with context.route(), and reuse contexts so the cache stays warm. Together those typically cut a run by 70–80%, which puts a browser workload within a small multiple of plain HTTP fetching.

Does this work in CI and with playwright.config?

Yes. The proxy is a normal config value, so you can set it per project in playwright.config and run the same specs against several markets. Give each parallel worker a distinct -sid- so workers do not share a device.

Does it work with Firefox and WebKit too?

Yes. The proxy option is part of Playwright's common launch and context API rather than a Chromium-specific feature, so the same configuration applies across all three browser engines.

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.