Mobile proxies with Puppeteer in 2026
Mobile proxies with Puppeteer in 2026
Mobile proxies with Puppeteer remains a go-to combo for Node-only browser automation in 2026, especially in shops that committed to Puppeteer years ago and have not migrated to Playwright. Puppeteer drives Chrome and Chromium with a simple, well-documented API, and pairs cleanly with SingaporeMobileProxy for Singapore-egress traffic on real SingTel, StarHub, and M1 carrier IPs. This guide covers every pattern that works in production: launch-time proxy config, per-page authentication, sticky sessions, route-based rotation, fingerprint coherence with mobile UA, stealth plugins, and the production-grade error handling that separates scrapers that survive a week from those that survive an hour.
The Puppeteer official docs live at pptr.dev. For comparison with other browser tools, see /blog/mobile-proxies-with-playwright-2026 and /blog/mobile-proxies-with-selenium-2026.
why Puppeteer with mobile proxies
Puppeteer is a Node library that launches and controls Chrome through the DevTools Protocol. Its strengths are a stable Chrome-only target, mature stealth plugin ecosystem, and tight integration with Node ergonomics. Compared to Playwright, the gap has narrowed but Puppeteer remains slightly more popular in pure-Node teams that do not need cross-browser support.
For mobile proxy work, the two patterns that matter are launch-time --proxy-server and runtime auth via page.authenticate. Mobile proxies behind a username and password require both, because Chrome itself does not handle the proxy-auth dialog without scripting.
Mobile proxies in Singapore route your browser through real cellular IPs that share carrier-grade NAT with thousands of other phones. The result is browser traffic that mostly looks like an ordinary commuter on the MRT, which is exactly what platforms like Shopee, Lazada, and TikTok expect of legitimate Singapore consumers.
the simplest Puppeteer with proxy
The minimum viable example is a Node script that launches Chrome with a SingaporeMobileProxy gateway, authenticates, and visits an IP echo. Save as simple.mjs.
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({
headless: "new",
args: [
"--proxy-server=http://gw.singaporemobileproxy.com:8001",
"--no-sandbox",
"--disable-blink-features=AutomationControlled",
],
});
const page = await browser.newPage();
await page.authenticate({
username: process.env.SMP_USER,
password: process.env.SMP_PASS,
});
await page.goto("https://api.ipify.org?format=json");
console.log(await page.content());
await browser.close();
The --proxy-server arg applies to the whole browser process. page.authenticate provides credentials when Chrome is challenged. The --disable-blink-features=AutomationControlled flag removes the most obvious headless tell.
per-browser rotation
Puppeteer does not support per-context proxies the way Playwright does. To run multiple isolated sessions on different egress IPs, launch multiple browsers in parallel. Each browser carries its own proxy.
async function makeBrowser(port) {
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=http://gw.singaporemobileproxy.com:${port}`, "--no-sandbox"],
});
return browser;
}
const ports = [8001, 8002, 8003, 8004];
const browsers = await Promise.all(ports.map(makeBrowser));
for (const b of browsers) {
const page = await b.newPage();
await page.authenticate({ username: process.env.SMP_USER, password: process.env.SMP_PASS });
await page.goto("https://api.ipify.org?format=json");
console.log(await page.content());
}
await Promise.all(browsers.map(b => b.close()));
Each browser is a separate Chrome process with its own memory, its own cookies, its own egress IP. The cost is RAM: budget 200 to 400 MB per browser at idle, more under load. For five to ten parallel sessions this is fine, beyond that consider Playwright with its per-context model.
per-page rotation through request interception
For per-request rotation, intercept network calls and route them through different proxies. The pattern uses page.setRequestInterception(true) and request.continue or request.respond.
import { fetch, ProxyAgent } from "undici";
const proxies = Array.from({ length: 20 }, (_, i) =>
`http://user:pass@gw.singaporemobileproxy.com:${8001 + i}`
);
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on("request", async req => {
if (["image", "font", "media"].includes(req.resourceType())) {
return req.abort();
}
const proxy = proxies[Math.floor(Math.random() * proxies.length)];
try {
const r = await fetch(req.url(), {
method: req.method(),
headers: req.headers(),
body: req.postData(),
dispatcher: new ProxyAgent(proxy),
redirect: "manual",
});
const buf = Buffer.from(await r.arrayBuffer());
await req.respond({
status: r.status,
headers: Object.fromEntries(r.headers),
body: buf,
});
} catch {
await req.abort();
}
});
await page.goto("https://example.com");
The pattern is heavier than launch-time rotation because every request flows through Node’s HTTP stack, but it gives you per-request egress control. Skip rendering of images and fonts to keep latency down. Reserve this for SERP scraping; for account work, stay with launch-time rotation.
sticky sessions in Puppeteer
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 following /blog/mobile-proxy-sticky-sessions-2026.
import crypto from "node:crypto";
function sessionProxy(accountId) {
const token = crypto.createHash("sha1").update(accountId).digest("hex").slice(0, 12);
return {
server: "http://gw.singaporemobileproxy.com:8000",
username: `${process.env.SMP_USER}-session-${token}`,
password: process.env.SMP_PASS,
};
}
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${sessionProxy("acct_1").server}`],
});
const page = await browser.newPage();
await page.authenticate({
username: sessionProxy("acct_1").username,
password: sessionProxy("acct_1").password,
});
The deterministic hash means every restart picks the same egress IP for the same account. Cookies and IP reputation stay aligned across runs.
fingerprint coherence on mobile IPs
A Singapore mobile IP plus a desktop UA looks instantly suspicious. Match the UA, viewport, and client hints to a real mobile device.
await page.emulate({
viewport: { width: 412, height: 915, deviceScaleFactor: 2.625, isMobile: true, hasTouch: true },
userAgent: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
});
await page.setExtraHTTPHeaders({
"Accept-Language": "en-SG,en;q=0.9",
"Sec-Ch-Ua-Mobile": "?1",
"Sec-Ch-Ua-Platform": '"Android"',
});
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "languages", { get: () => ["en-SG", "en"] });
Object.defineProperty(navigator, "platform", { get: () => "Linux armv8l" });
});
Or use Puppeteer’s built-in device descriptor:
import { KnownDevices } from "puppeteer";
await page.emulate(KnownDevices["Pixel 5"]);
That single line covers UA, viewport, DPR, mobile flag, and touch flag. Add timezone and locale via CDP:
const client = await page.target().createCDPSession();
await client.send("Emulation.setTimezoneOverride", { timezoneId: "Asia/Singapore" });
await client.send("Emulation.setLocaleOverride", { locale: "en-SG" });
anti-detection with puppeteer-extra-plugin-stealth
Vanilla Puppeteer leaves dozens of detection signals visible. The puppeteer-extra-plugin-stealth package patches them.
import puppeteerExtra from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
puppeteerExtra.use(StealthPlugin());
const browser = await puppeteerExtra.launch({
headless: "new",
args: ["--proxy-server=http://gw.singaporemobileproxy.com:8001"],
});
Stealth fixes navigator.webdriver, navigator.plugins, WebGL vendor strings, chrome.runtime, and canvas hashes. Combined with a coherent mobile UA and a real Singapore mobile IP, this gets through most production detection libraries.
cookies and session resume
Puppeteer can dump cookies to a file and reload them on the next run.
async function saveSession(page, file) {
const cookies = await page.cookies();
const storage = await page.evaluate(() => JSON.stringify(localStorage));
await fs.writeFile(file, JSON.stringify({ cookies, storage }));
}
async function restoreSession(page, file) {
const data = JSON.parse(await fs.readFile(file, "utf8"));
await page.setCookie(...data.cookies);
await page.evaluate(s => {
Object.entries(JSON.parse(s)).forEach(([k, v]) => localStorage.setItem(k, v));
}, data.storage);
}
Combine with sticky session proxies keyed on account ID and you have a setup that resumes cleanly across crashes and deploys.
comparison: Puppeteer vs Playwright for mobile proxy work
| feature | Puppeteer | Playwright |
|---|---|---|
| browsers | Chrome only | Chromium, Firefox, WebKit |
| proxy config | per-browser | per-context |
| auth handling | page.authenticate | proxy options |
| stealth ecosystem | mature | growing fast |
| device presets | KnownDevices | devices, more options |
| docs and community | huge | huge |
| best for | Chrome-only legacy | new projects |
Pick Puppeteer if your team is already deep in it. Pick Playwright for any new browser scraping project. Both work well with SingaporeMobileProxy, but Playwright’s per-context model is materially better for multi-account work.
production checklist
Run headless in production, headed for debugging. Pass --no-sandbox only inside Docker where it is required. Watch memory: each Puppeteer browser is a real Chrome process and they accumulate. Restart browsers every few hundred pages. Use browser.process().kill("SIGKILL") if a graceful close hangs. Set explicit navigation timeouts: page.goto(url, { timeout: 30000 }). Catch TimeoutError and rotate, not retry. Block heavy resource types (images, fonts, media) when scraping HTML to cut bandwidth and latency. Always close pages and contexts you create, even on error paths.
For Kubernetes deployment, give each pod 1.5 to 2 GB of memory and one to two browsers. Use a horizontal scaler keyed on queue depth. Liveness probes should hit a Node HTTP endpoint, not the browser, so a stuck browser does not kill the pod prematurely.
faq
can Puppeteer use per-context proxies? Not directly. Each browser instance has one proxy. Use multiple browsers for multiple egress IPs.
do I need stealth on mobile proxies? Yes. Mobile proxies fix the IP signal, stealth fixes the JavaScript signals. They are complementary.
how many Puppeteer browsers can I run on one machine? Memory-bound. Budget 200 to 400 MB per browser at idle, more under load. Eight to twelve fits comfortably on a 16 GB box.
how do I rotate proxies without restarting Puppeteer? Use the request interception pattern shown above, or close the browser and launch a new one with a different proxy.
why does page.authenticate need to be called per page? Because the auth handler is bound to a page lifecycle. Set it on every page you create.
try it on real Singapore IPs
Spin up a SingaporeMobileProxy trial and drop any of these snippets into a fresh Node project. Swap the gateway hostname for your endpoint and run. Our /api-docs covers sticky sessions, port-level rotation, and the bandwidth metrics endpoints you will want to watch in production.