Playwright mobile proxies: the setup that actually gets past Shopee and Lazada
The IP is the problem, not your headers
If your Playwright scraper keeps getting banned on Shopee, the instinct is to tweak headers again. That’s not where the ban is coming from. It’s the egress IP.
Datacenter proxies stopped working against any serious bot defence a long time ago. Residential is mid at best. What actually holds up is mobile proxies on real Singtel, StarHub, or M1 IPs. These sit behind carrier-grade NAT, so you’re sharing egress with thousands of ordinary phones instead of a data center subnet that every fraud team already has flagged.
This setup works against Shopee, Lazada, TikTok, and Instagram as of 2026. Below is the exact Playwright configuration: per-context proxies, sticky sessions, mobile fingerprint coherence, and stealth patched on top.
There are three parts to get right. First, the minimum viable Playwright launch with a mobile proxy. Second, per-context rotation for multi-account work. Third, fingerprint coherence and stealth, because the IP alone isn’t enough.
The minimum launch
Start with the smallest thing that actually works: Python, Chromium, one proxy, one mobile fingerprint. The proxy has to go on the browser launch, not on the page.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(
proxy={
"server": "http://gateway.example:port",
"username": "user",
"password": "pass",
}
)
context = browser.new_context(
viewport={"width": 390, "height": 844},
device_scale_factor=3,
locale="en-SG",
timezone_id="Asia/Singapore",
)
Every line here matters. The viewport, the device scale factor, the en-SG locale, the Asia/Singapore timezone. All of them have to agree with the mobile IP you’re routing through. One mismatch and the whole picture cracks: a Singapore mobile IP with a US timezone is an immediate flag for anything paying attention.
Per-context rotation for multi-account work
This is the pattern that pays for itself once you’re running more than one account. One Playwright browser, many contexts, each context on its own proxy port. Each context gets its own cookies, its own storage, and its own egress IP.
context_a = browser.new_context(proxy={"server": "http://gateway.example:7001", ...})
context_b = browser.new_context(proxy={"server": "http://gateway.example:7002", ...})
That’s what a real user with multiple devices actually looks like: separate sessions, separate IPs, no shared state bleeding between accounts.
storage_state is what makes this survive restarts. Log in once, save the cookies and storage to a file, and reload that file on the next run instead of logging in fresh every time.
context.storage_state(path="account_a.json")
# next run
context = browser.new_context(storage_state="account_a.json", proxy={...})
Paired with sticky sessions, the same account keeps landing on the same IP every time it runs.
Sticky sessions, deterministic IPs
Sticky is the piece that makes the cookies mean something. Bake a hash of the account ID into the proxy username so the same account always resolves to the same egress IP.
import hashlib
def sticky_username(account_id, base_user):
session_key = hashlib.sha256(account_id.encode()).hexdigest()[:8]
return f"{base_user}-session-{session_key}"
There are five carriers available in Singapore, but for any single account you want exactly one IP, every run. Restart the process, kill the box, come back the next day: same egress IP. Cookies and IP reputation stay aligned, which is the whole point for account-bound work. If the IP shifts under an account that already has session history, you’ve just handed the platform a mismatch to notice.
Fingerprint coherence
A mobile IP paired with a desktop user agent is the single dumbest tell in the book, and most setups still ship it. The proxy says you’re a phone in Singapore. The user agent says you’re a Mac in California. The client hints say something else again. Detection libraries chain these signals together, so any one mismatch gives the whole thing away.
The fix is one line: use Playwright’s built-in device preset.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
iphone = p.devices["iPhone 13"]
context = browser.new_context(
**iphone,
locale="en-SG",
timezone_id="Asia/Singapore",
geolocation={"latitude": 1.3521, "longitude": 103.8198},
permissions=["geolocation"],
)
That single line gives you the user agent, viewport, device scale factor, mobile flag, touch flag, and the correct client hints all in one coherent bundle. Add geolocation and permissions, and the fingerprint matches the IP end to end.
Stealth on top
Stealth plugins handle what the proxy and the device preset can’t touch: the JavaScript-level signals. The webdriver flag, navigator.plugins, the WebGL vendor string, canvas hashing. These are the things automation frameworks leave behind by default, and they’re checked independently of where your traffic is coming from.
Stealth isn’t a silver bullet on its own. But a coherent mobile fingerprint, layered on a real Singapore mobile IP, layered with stealth patches, is what gets through most production bot defences in 2026.
Production gotchas
A few things that only show up once you’re running this for real, not in a demo.
Set explicit timeouts on every goto call. Thirty seconds is a sane default. When a TimeoutError fires, don’t just retry on the same IP, rotate the proxy. A timeout on a mobile connection is often the carrier or the target site pushing back, and retrying blind just burns time on a connection that’s already flagged or dead.
Keep one browser process with many contexts rather than spawning a new browser per account. Process spawn cost is real, and it adds up fast once you’re running dozens of accounts in parallel.
Discard and recreate contexts after roughly a thousand pages so memory stays bounded. Long-lived contexts accumulate state that eventually shows up as memory pressure or subtle scraping failures.
And don’t hammer one port with twenty contexts. Mobile proxies are real cellular connections with finite bandwidth, not an infinitely scalable pool. Spread contexts across many ports instead. Five to ten contexts per browser process is the practical ceiling before you start starving individual sessions of bandwidth.
Where this actually runs
None of this works without an underlying pool of real mobile IPs across multiple carriers, with sticky sessions built into how you request a port in the first place. If you’re setting up a Playwright stack like the one above, that’s the piece worth sorting out first, before you write a single line of scraping logic.
If you want to see how this looks running against real Singapore carrier IPs, take a look at what we run at Singapore Mobile Proxy.
Get new guides and videos first — join the Telegram channel.