← back to blog

Mobile proxies with Playwright complete 2026 guide

playwright mobile-proxy browser scraping automation

Mobile proxies with Playwright complete 2026 guide

Mobile proxies with Playwright is the pairing that actually works in 2026 for any browser automation that touches Shopee, Lazada, TikTok, Instagram, Facebook, Carousell, or any other site with serious bot defences. Playwright gives you headless and headed browser control across Chromium, Firefox, and WebKit. Mobile proxies put you on real SingTel, StarHub, and M1 IPs that look like ordinary Singapore consumers behind carrier-grade NAT. Together they handle 90 percent of multi-account, ad-verification, and SERP work that pure HTTP scraping cannot.

This guide covers every pattern you need to ship a Playwright + SingaporeMobileProxy stack to production: per-context proxies, per-request rotation through routes, sticky sessions for account-bound flows, fingerprint coherence with mobile UA, cookie persistence across sessions, and the anti-detection details that separate scrapers that survive a week from those that survive an hour.

If Playwright is not the tool for your stack, see /blog/mobile-proxies-with-puppeteer-2026 and /blog/mobile-proxies-with-selenium-2026 for the same patterns in those frameworks. The mobile proxy primer is at /blog/what-is-a-mobile-proxy.

why Playwright with mobile proxies works

Playwright was built by ex-Puppeteer engineers at Microsoft to fix the operational pain of running headless Chrome at scale. It supports three browser engines, has first-class support for multiple browser contexts inside a single browser process, and exposes a clean API for proxies, cookies, geolocation, and viewport. The official documentation lives at playwright.dev.

For mobile proxy work, three Playwright features matter most. First, per-context proxies let you run multiple isolated sessions inside one browser, each with its own egress IP. Second, the route handler lets you intercept and modify network traffic, which you can use for fine-grained per-request rotation. Third, the device emulation presets give you coherent mobile fingerprints that match the mobile-IP egress.

The combination produces traffic that is hard to distinguish from a real user on a real Singapore phone. That is the whole point.

the simplest Playwright with proxy

The minimum viable example is a Python script that launches Chromium with a SingaporeMobileProxy gateway and visits an IP echo service. Save as simple_playwright.py.

import os
from playwright.sync_api import sync_playwright

PROXY = {
    "server": "http://gw.singaporemobileproxy.com:8001",
    "username": os.environ["SMP_USER"],
    "password": os.environ["SMP_PASS"],
}

with sync_playwright() as p:
    browser = p.chromium.launch(proxy=PROXY, headless=True)
    context = browser.new_context(
        user_agent="Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/120.0.0.0",
        viewport={"width": 412, "height": 915},
        device_scale_factor=2.625,
        is_mobile=True,
        has_touch=True,
        locale="en-SG",
        timezone_id="Asia/Singapore",
    )
    page = context.new_page()
    page.goto("https://api.ipify.org?format=json")
    print(page.content())
    browser.close()

The proxy argument on launch applies to every context in the browser. The context options together produce a coherent mobile fingerprint: Pixel 8 viewport and DPR, en-SG locale, Asia/Singapore timezone, mobile UA. Pair that with a Singapore mobile IP and the page sees a consistent picture.

The same in Node:

import { chromium } from "playwright";

const browser = await chromium.launch({
  proxy: {
    server: "http://gw.singaporemobileproxy.com:8001",
    username: process.env.SMP_USER,
    password: process.env.SMP_PASS,
  },
  headless: true,
});

const context = await browser.newContext({
  userAgent: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36",
  viewport: { width: 412, height: 915 },
  deviceScaleFactor: 2.625,
  isMobile: true,
  hasTouch: true,
  locale: "en-SG",
  timezoneId: "Asia/Singapore",
});
const page = await context.newPage();
await page.goto("https://api.ipify.org?format=json");
console.log(await page.content());
await browser.close();

Both versions produce identical traffic. Pick whichever language your team uses.

per-context rotation with multiple sessions

The most powerful pattern for multi-account work is one context per account. Each context gets its own proxy, its own cookies, its own storage, and its own egress IP. This is exactly what platforms expect when a real user has multiple devices.

from playwright.sync_api import sync_playwright

ACCOUNTS = [
    {"id": "shop_acct_1", "port": 8001},
    {"id": "shop_acct_2", "port": 8002},
    {"id": "shop_acct_3", "port": 8003},
]

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    contexts = []
    for acct in ACCOUNTS:
        ctx = browser.new_context(
            proxy={
                "server": f"http://gw.singaporemobileproxy.com:{acct['port']}",
                "username": os.environ["SMP_USER"],
                "password": os.environ["SMP_PASS"],
            },
            storage_state=f"state_{acct['id']}.json" if os.path.exists(f"state_{acct['id']}.json") else None,
            user_agent="Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36",
            is_mobile=True,
            locale="en-SG",
            timezone_id="Asia/Singapore",
        )
        contexts.append((acct, ctx))
    for acct, ctx in contexts:
        page = ctx.new_page()
        page.goto("https://shopee.sg/")
        ctx.storage_state(path=f"state_{acct['id']}.json")
    browser.close()

This pattern is the bread and butter of the /blog/tiktok-shopee-lazada-singapore playbook for multi-account commerce. Storage state persists cookies and localStorage between runs, which means you log in once and reuse the session for hours or days.

per-request rotation through routes

For pure scraping where you want a different IP per HTTP call, Playwright’s route handler is your tool. Routes intercept every request the page makes and let you decide what to do with it. The trick is to combine routes with a fetch through your proxy pool, then return the response back to the page.

from playwright.sync_api import sync_playwright
import requests, random

def make_handler(pool):
    def handler(route, request):
        if request.resource_type in ("image", "font", "media"):
            return route.abort()
        proxy = random.choice(pool)
        r = requests.request(
            request.method,
            request.url,
            headers=request.headers,
            data=request.post_data,
            proxies={"http": proxy, "https": proxy},
            timeout=20,
            allow_redirects=False,
        )
        route.fulfill(status=r.status_code, headers=dict(r.headers), body=r.content)
    return handler

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    ctx = browser.new_context(is_mobile=True)
    page = ctx.new_page()
    page.route("**/*", make_handler(["http://user:pass@gw.singaporemobileproxy.com:8001",
                                       "http://user:pass@gw.singaporemobileproxy.com:8002"]))
    page.goto("https://example.com/")
    print(page.title())
    browser.close()

This pattern is heavier than per-context rotation because every request flows through Python instead of the browser’s native network stack, but it gives you per-request egress IP control. Use it for SERP scraping where rotation per request is the whole point. Skip it for account-bound work where you want a single sticky IP per session.

sticky sessions in Playwright

For account-bound flows, sticky sessions on SingaporeMobileProxy give you a stable mobile IP for the lifetime of a session. Bake the session token into the proxy username, exactly the pattern documented in /blog/mobile-proxy-sticky-sessions-2026.

import hashlib

def session_proxy(account_id):
    token = hashlib.sha1(account_id.encode()).hexdigest()[:12]
    return {
        "server": "http://gw.singaporemobileproxy.com:8000",
        "username": f"{os.environ['SMP_USER']}-session-{token}",
        "password": os.environ["SMP_PASS"],
    }

ctx = browser.new_context(proxy=session_proxy("acct_123"))

The deterministic hash means every restart picks up the same egress IP for the same account. That keeps cookies, fingerprints, and IP reputation aligned across runs.

fingerprint coherence on mobile IPs

A mobile proxy with a desktop UA looks instantly suspicious. The proxy says you are a phone in Singapore, the UA says you are a desktop in California, and the JavaScript-visible properties say something else again. Get all three to agree and the picture is clean.

ctx = browser.new_context(
    user_agent="Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
    viewport={"width": 412, "height": 915},
    device_scale_factor=2.625,
    is_mobile=True,
    has_touch=True,
    locale="en-SG",
    timezone_id="Asia/Singapore",
    geolocation={"latitude": 1.3521, "longitude": 103.8198},
    permissions=["geolocation"],
    color_scheme="light",
    extra_http_headers={
        "Accept-Language": "en-SG,en;q=0.9",
        "Sec-Ch-Ua-Mobile": "?1",
        "Sec-Ch-Ua-Platform": '"Android"',
    },
)

The two often-missed details are client hints (Sec-Ch-Ua-*) and Accept-Language. Both must match the mobile UA. Modern Chromium emits client hints automatically on every request, but the values come from the underlying browser build, which means a desktop Chromium reporting Android client hints is a tell. Playwright’s mobile device presets (p.devices["Pixel 7"]) get this right out of the box.

device = p.devices["Pixel 7"]
ctx = browser.new_context(**device, proxy=PROXY)

That single line gives you a complete coherent fingerprint with no manual tuning.

anti-detection: stealth plugins and beyond

Playwright by default does not patch the dozens of small JavaScript fingerprint signals that detection libraries check. The playwright-stealth package fills the gap. In Python:

pip install playwright-stealth
from playwright_stealth import stealth_sync

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    ctx = browser.new_context(**p.devices["Pixel 7"], proxy=PROXY)
    page = ctx.new_page()
    stealth_sync(page)
    page.goto("https://bot.sannysoft.com/")

In Node, the equivalent is playwright-extra with the stealth plugin:

import { chromium } from "playwright-extra";
import stealth from "puppeteer-extra-plugin-stealth";

chromium.use(stealth());
const browser = await chromium.launch({ proxy: PROXY });

Stealth plugins fix the most common detection vectors: webdriver flag, navigator.plugins, navigator.languages, WebGL vendor, canvas hashing, and battery API. They are not a silver bullet but they raise the floor enough that a coherent mobile fingerprint plus a real Singapore mobile IP gets through most defences.

production patterns: cookies, storage, and resume

A scraper that loses its session on every restart is unusable. Playwright’s storage_state captures cookies and localStorage to a JSON file you can reload.

ctx.storage_state(path="state.json")
# next run
ctx = browser.new_context(storage_state="state.json", proxy=PROXY)

Combine that with a sticky session proxy keyed on the same account ID and you have a setup that resumes cleanly across runs and machine restarts. Store both files together: state_{acct}.json and the account-to-port mapping. When you want to evict a session, delete both.

For high reliability, snapshot storage_state every N pages, not just at exit. Crash-safe checkpointing keeps you from losing hours of session warming.

comparison: Playwright vs other browser tools

tool proxy support per-context proxies mobile emulation speed best for
Playwright first-class yes excellent fast most browser scraping in 2026
Puppeteer first-class per-browser only good fast Chrome-only stacks
Selenium basic no manual slower legacy and cross-browser
Cypress limited no partial variable testing not scraping
Patchright first-class yes excellent fast hardened anti-detection

Pick Playwright as the default for any new browser scraping or automation project. It has caught up with and surpassed Puppeteer on every metric that matters for scraping. Selenium remains relevant only when you must support cross-browser legacy frameworks. See /blog/mobile-proxies-with-puppeteer-2026 and /blog/mobile-proxies-with-selenium-2026 for those.

production checklist

Run Playwright in headless mode in production but test in headed mode locally. Headed reveals visual bugs that headless hides. Use slow_mo only for debugging, never in production. Set explicit timeouts on every navigation: page.goto(url, timeout=30_000). Catch TimeoutError and rotate the proxy, not just retry. Keep one browser process and many contexts rather than many processes, because process spawn cost is real. Discard contexts after a thousand pages or so to keep memory bounded. Use browser.contexts() to inspect what is open. For Kubernetes deployment, set memory limits at 1.5 to 2 GB per browser pod, depending on context count.

The other production gotcha is concurrent contexts on the same proxy. Mobile proxies are real cellular connections with finite bandwidth. Hammering one port with twenty contexts will throttle. Spread contexts across many ports.

faq

which Playwright language binding is best with mobile proxies? Both Python and Node bindings have feature parity. Pick the language your team already uses.

can I rotate proxies without restarting the browser? Yes through the route handler pattern shown above, or by closing and recreating contexts. The browser process itself stays up.

do I need stealth plugins on mobile proxies? Highly recommended. Mobile IPs raise the floor against detection, but stealth plugins fix the JavaScript signals that mobile IPs cannot.

how many concurrent contexts can I run? Empirically 5 to 10 per browser process before memory becomes the bottleneck. Use multiple browser processes if you need more.

what is the best way to debug a Playwright + proxy issue? Run in headed mode, open devtools, watch the network panel. If the request never leaves the browser, your proxy config is wrong. If it leaves but never returns, your proxy or target is the issue.

try it on real Singapore IPs

Spin up a SingaporeMobileProxy trial and copy any of these snippets. Swap the gateway hostname and ship. Our /api-docs cover sticky sessions, port-level rotation, and the device emulation presets that pair best with a coherent mobile fingerprint.

ready to try Singapore mobile proxies?

2-hour free trial. no credit card required.

start free trial
message me on telegram