JustProxies

Patterns

Puppeteer & Playwright

5-min readpuppeteer-playwright
Headless-browser flows have an extra layer of plumbing — the browser owns the connection, not your code. Both Playwright and Puppeteer expose proxy configuration; the trick is knowing which scope to attach it at.

Playwright — per-context proxy

Set the proxy at browser.newContext(). Each context can have its own proxy, which is the cleanest way to fan out across many sessions without juggling multiple browsers.

playwright-context.jsjavascript
import { chromium } from "playwright";

const browser = await chromium.launch({ headless: true });

const ctx = await browser.newContext({
  proxy: {
    server:   "http://gw.justproxies.online:8080",
    username: "USER",
    password: "PASS",
  },
});

const page = await ctx.newPage();
await page.goto("https://api.ipify.org");
console.log(await page.textContent("body"));
await browser.close();

Playwright — fixture per worker

For test runners or batch jobs, parameterise the context fixture with a sticky session token derived from the worker index — every worker gets its own IP, every test gets repeatable affinity.

playwright.config-style.jsjavascript
import { test as base, chromium } from "@playwright/test";

export const test = base.extend({
  context: async ({}, use, testInfo) => {
    const session = `worker-${testInfo.workerIndex}-${testInfo.testId.slice(0,6)}`;
    const browser = await chromium.launch();
    const ctx = await browser.newContext({
      proxy: {
        server:   "http://gw.justproxies.online:8080",
        username: `USER-session-${session}`,
        password: "PASS",
      },
    });
    await use(ctx);
    await browser.close();
  },
});

Puppeteer — launch arg

Puppeteer takes the proxy at the --proxy-server launch flag. The flag is browser-wide; for parallelism, launch multiple browsers.

puppeteer-launch.jsjavascript
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
  args: ["--proxy-server=http://gw.justproxies.online:8080"],
});

const page = await browser.newPage();
await page.authenticate({ username: "USER", password: "PASS" });

await page.goto("https://api.ipify.org");
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();

Puppeteer — proxy auth

Puppeteer doesn't accept inline credentials in the launch arg. Call page.authenticate() per page, before the first navigation.

Order matters
page.authenticate() must run before any page.goto() — the credentials apply to the next request the page makes.

Mobile profile + mobile pool

For full carrier-NAT realism: set the browser to a mobile device emulation and route through the mobile pool. The two operate independently — emulation sets headers/touch/viewport, the proxy sets the egress IP.

mobile-realism.jsjavascript
import { chromium, devices } from "playwright";

const browser = await chromium.launch();
const ctx = await browser.newContext({
  ...devices["Pixel 7"],
  proxy: {
    server:   "http://gw.justproxies.online:8080",
    username: "MOBILE_USER-session-S1",
    password: "MOBILE_PASS",
  },
});

const page = await ctx.newPage();
await page.goto("https://m.target.com/");
// Mobile UA, mobile viewport, mobile carrier NAT egress.

Stealth and fingerprinting

We don't bundle a stealth plugin or rewrite browser fingerprints — that is target-specific work and changes faster than we can ship. The two pieces we recommend:

  • Pool selection — pick the right pool for the target. See /products for the trust-score breakdown.
  • Per-context isolation — never reuse a context across sessions you want isolated. Storage state, cookies and cache leak between.

For fingerprint patching specifically, the open-source puppeteer-extra-plugin-stealth and the Playwright equivalents are the usual starting points.

Found a gap, or something wrong?
A real human reads support email.