How to Use a Proxy with Playwright for Reproducible Geo-Testing
Our guides are based on hands-on testing and verified sources. Each article is reviewed for accuracy and updated regularly to ensure current, reliable information.Read our editorial policy.
A web application can work perfectly in your development environment and still behave differently for users in another country, state, or city.
The symptom might be a different price, a missing product, an unexpected redirect, a payment method that never appears, or a localized page that does not match what your team sees.
Reproducing the difference is only the first step. The harder problem is identifying which environmental signal caused it.
A website can respond to public IP location, browser geolocation, locale, timezone, cookies, account settings, CDN routing, cached content, feature flags, or third-party services. Change several of these at once and you may reproduce the bug without discovering its cause.
This tutorial shows how to use a proxy with Playwright to build a reproducible geo-testing workflow where those variables are controlled independently.
If you need a broader troubleshooting workflow before automating the test, CodeItBro’s guide to reproducing website bugs that only happen in another country covers logs, CDN behavior, payments, consent systems, and other regional failure modes.
Use geo-targeted proxies only for applications and workflows you are authorized to test, and follow applicable site terms, privacy requirements, and data-handling policies.
What Are You Actually Testing?
Suppose a customer reports:
“The checkout page shows a different offer when I access it from Germany.”
Several independent signals could explain the result:
- The server looks up the visitor’s public IP address.
- The browser exposes German coordinates through the Geolocation API.
- The browser sends a German locale.
- The browser is using a European timezone.
- The customer’s account is registered in Germany.
- A cookie stores a previous regional preference.
- A CDN edge returns different cached content.
- A country-specific feature flag is active.
- A payment, fraud, tax, or inventory service behaves differently by region.
These inputs are related to location, but they are not interchangeable.
| Signal | Where It Comes From | Common Uses | How to Control It |
|---|---|---|---|
| Exit IP | Network/proxy | Country targeting, CDN routing, fraud checks, regional pricing | Regional proxy exit |
| Browser geolocation | Geolocation API | Nearby stores, maps, delivery areas, geofencing | Playwright geolocation |
| Locale | Browser settings | Language, formatting, localized content | Playwright locale |
| Timezone | Browser/OS environment | Dates, promotions, schedules, availability | Playwright timezoneId |
| Cookies/storage | Browser state | Preferences, experiments, previous market selection | Browser contexts and storage state |
| Account data | Application database | Billing country, eligibility, tax, inventory | Controlled test accounts |
The central rule is simple: change one variable at a time whenever possible.
Step 1: Set Up a Clean Playwright Project
Install Playwright Test and dotenv:
npm install -D @playwright/test dotenv
npx playwright installKeep proxy credentials and target URLs outside your source code. Create a .env file:
PROXY_SERVER=http://proxy-host:port
PROXY_USER=username
PROXY_PASS=password
TARGET_URL=https://example.com
TEST_LOCALE=en-US
TEST_TIMEZONE=America/New_YorkAdd .env to .gitignore.
If you are unfamiliar with proxy URLs, protocols, ports, authentication, or session behavior, CodeItBro’s proxy configuration guide explains the core pieces and common setup mistakes.
Step 2: Configure the Proxy Separately From Locale and Timezone
Playwright supports HTTP(S) and SOCKS proxies and can apply proxy settings to browser traffic. Its official network documentation covers global and per-context proxy configuration.
A basic playwright.config.ts can look like this:
import 'dotenv/config';
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
proxy: {
server: process.env.PROXY_SERVER!,
username: process.env.PROXY_USER,
password: process.env.PROXY_PASS,
},
locale: process.env.TEST_LOCALE || 'en-US',
timezoneId: process.env.TEST_TIMEZONE || 'America/New_York',
trace: 'retain-on-failure',
},
});These settings control different parts of the environment.
The proxy controls the network route and visible exit IP. The locale influences browser language signals and formatting. The timezone changes the timezone seen by the page.
Changing the proxy does not automatically change the browser’s timezone, locale, or Geolocation API coordinates.
For a quick manual sanity check, CodeItBro’s browser timezone detector reads the timezone configured in the browser rather than inferring it from the IP address.
Step 3: Treat Browser Geolocation as a Separate Test Input
Browser geolocation is easy to overlook because developers often use “geolocation” to mean IP location.
They are different.
A site using:
navigator.geolocation.getCurrentPosition(...)is asking the browser for coordinates. Changing only the exit IP does not necessarily change those coordinates.
Playwright can emulate coordinates and grant geolocation permission independently:
import { test } from '@playwright/test';
test.use({
geolocation: {
latitude: 52.52,
longitude: 13.405,
},
permissions: ['geolocation'],
});
test('Berlin geolocation', async ({ page }) => {
await page.goto(process.env.TARGET_URL!);
});Playwright’s official configuration documentation treats geolocation, locale, timezone, and permissions as separate emulation settings.
This lets you test useful combinations such as:
- German IP + US locale + US timezone + no browser coordinates
- US IP + German locale
- US IP + Berlin browser coordinates
- German IP + German locale + Berlin timezone + Berlin coordinates
If only one of those combinations changes the application, you have narrowed the cause substantially.
Step 4: Verify the Environment Before Testing the Application
Do not assume that requesting a German, US, or city-specific proxy exit means the destination actually sees that location.
Record the visible exit IP first:
import { test } from '@playwright/test';
test('record test environment', async ({ page }) => {
await page.goto('https://api.ipify.org?format=json');
const { ip } = JSON.parse(
await page.locator('body').innerText()
);
const browserEnv = await page.evaluate(() => ({
language: navigator.language,
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
userAgent: navigator.userAgent,
}));
console.log({
exitIp: ip,
...browserEnv,
});
});Then check how the relevant geolocation database classifies that IP.
Where useful, record:
- exit IP
- reported country
- reported region/state
- reported city
- ASN or ISP
- the IP-geolocation service used
- proxy/session identifier
- browser locale
- browser timezone
- browser coordinates, if enabled
- browser version
- timestamp
- test-run ID
City-level IP geolocation is approximate and can differ between databases. If your application uses a specific GeoIP provider, compare the proxy exit against that same provider whenever possible.
Step 5: Capture the Network Traffic That Matters
A screenshot proves that two pages look different. It usually does not explain why.
HTTP requests and responses often expose the actual difference: a changed API payload, redirect, cache state, currency, feature flag, inventory response, or payment configuration.
You can monitor important responses in Playwright:
import { test } from '@playwright/test';
test('capture important responses', async ({ page }) => {
page.on('response', response => {
const url = response.url();
if (!/\/api\/|\/pricing|\/checkout/.test(url)) {
return;
}
const headers = response.headers();
console.log({
url,
status: response.status(),
contentType: headers['content-type'],
cacheControl: headers['cache-control'],
age: headers['age'],
vary: headers['vary'],
cdnCache:
headers['cf-cache-status'] ||
headers['x-cache'],
});
});
await page.goto(process.env.TARGET_URL!, {
waitUntil: 'load',
});
});For location-sensitive applications, watch:
- pricing APIs
- localization endpoints
- inventory responses
- redirects
- feature-flag services
- checkout and payment calls
- authentication requests
- CDN cache headers
- cookies
- currency data
- tax or shipping responses
Two pages can look identical while an API response contains a different region, price, experiment ID, or availability flag.
If you need to inspect a sample payload manually, CodeItBro’s JSON Formatter can make sanitized API responses easier to read.
Do not indiscriminately log every response body. Authentication, checkout, and customer APIs can contain tokens, account information, addresses, or other sensitive data.
Step 6: Keep Stateful User Journeys on a Stable Exit IP
Not every geo-test should rotate to a new IP for every request.
Consider this journey:
- Open a product page.
- Add an item to the cart.
- Begin checkout.
- Load available payment methods.
- Confirm the final price.
Those requests belong to one user journey.
If the IP changes halfway through the flow, the test itself may introduce behavior that a normal user would not see. Fraud systems, authentication services, checkout providers, or regional routing logic may react to the sudden change.
For multi-step flows, use a sticky session designed to preserve the same exit IP throughout the journey. Rotation is more appropriate between independent observations, such as checking a public page across several exits.
The practical rule is:
Rotate between independent observations; preserve the exit IP within a stateful journey.
When selecting a proxy layer, verify both geographic targeting and session controls. At the time of writing, ColdProxy supports country, state, and city targeting on its Residential IPv4 plans along with rotating and sticky sessions.
A sticky session should still be verified during critical tests. Proxy infrastructure can fail over, and an exit IP may be reassigned if the current exit becomes unavailable.
Step 7: Build a Diagnostic Matrix
A controlled test matrix makes the cause much easier to identify.
| Test | Exit IP Region | Locale | Timezone | Browser Coordinates | Browser State |
|---|---|---|---|---|---|
| Baseline | United States | en-US | America/New_York | Not set | Clean |
| A | Germany | en-US | America/New_York | Not set | Clean |
| B | United States | de-DE | America/New_York | Not set | Clean |
| C | United States | en-US | Europe/Berlin | Not set | Clean |
| D | United States | en-US | America/New_York | Berlin | Clean |
| E | Germany | de-DE | Europe/Berlin | Berlin | Clean |
| F | Germany | de-DE | Europe/Berlin | Berlin | Existing |
If Test A changes the application while B, C, and D do not, the exit IP is the strongest candidate.
If Test B reproduces the problem without changing the IP, investigate locale-sensitive code.
If Test D alone changes the behavior, the site is likely using browser coordinates.
If only Test F reproduces it, stored browser or account state may be part of the cause.
Use explicit baseline values rather than labels such as “local.” A CI runner in Virginia, a laptop in India, and a cloud runner in Germany do not share the same local environment.
Step 8: Use Isolated Browser Contexts
Browser contexts give tests isolated cookies, local storage, session storage, permissions, and other browser state.
With the current Playwright Test browser fixture, a manually created browser.newContext() can inherit applicable use options from your Playwright configuration. You can then override the values that are part of the experiment.
import { test } from '@playwright/test';
test('German browser environment', async ({ browser }) => {
const context = await browser.newContext({
locale: 'de-DE',
timezoneId: 'Europe/Berlin',
geolocation: {
latitude: 52.52,
longitude: 13.405,
},
permissions: ['geolocation'],
});
const page = await context.newPage();
await page.goto(process.env.TARGET_URL!);
await context.close();
});If a particular test also needs different proxy settings, override the proxy option for that context.
If persistent user state is part of the test, save and restore it deliberately with Playwright’s storageState rather than allowing state to leak between unrelated runs.
Treat saved authentication state as sensitive. Storage-state files can contain cookies or other information that allows account access, so do not commit them to source control.
Step 9: Keep Evidence When a Test Fails
The trace: 'retain-on-failure' configuration from Step 2 is useful for intermittent regional bugs.
Playwright can retain traces containing browser actions, DOM snapshots, screenshots, console activity, timing, and network information for failed runs.
Open a saved trace with:
npx playwright show-trace path/to/trace.zipThe official Playwright Trace Viewer documentation explains how to inspect actions, network requests, DOM snapshots, console messages, and other recorded information.
Treat traces as potentially sensitive artifacts. They may expose API requests, response data, session information, page content, or customer data. Store them securely, avoid production customer information where possible, and redact credentials or personal data before sharing them.
Step 10: Test Several Exit IPs Before Blaming a Region
One failing German exit IP does not prove that Germany is affected.
The difference could come from:
- a single ISP
- a particular ASN
- an IP range
- IP reputation
- a CDN route
- a stale edge cache
- a temporary routing problem
Repeat the same controlled test through several independent exits in the target region.
If multiple exits reproduce the same behavior while a control region does not, the evidence for genuinely regional behavior becomes much stronger.
For state- or city-specific bugs, repeat the same principle at that level instead of assuming that one IP represents the entire city or state.
Step 11: Compare API Responses Programmatically
Structured comparisons are usually more useful than screenshots when the actual difference lives in an API response.
Start waiting for the response before triggering the navigation or action so that the test does not miss it:
const pricingPromise = page.waitForResponse(response =>
response.url().includes('/api/pricing')
);
await page.goto(process.env.TARGET_URL!);
const pricingResponse = await pricingPromise;
const pricing = await pricingResponse.json();
console.log(
JSON.stringify(pricing, null, 2)
);Compare fields such as:
- currency
- country
- region
- price
- product availability
- tax configuration
- payment methods
- campaign ID
- experiment ID
- redirect URL
For one-off debugging, you can paste sanitized responses into CodeItBro’s JSON Compare tool. It compares the parsed structure instead of treating different key ordering as a meaningful change.
Step 12: Save a Reproducibility Record
A reproducible test should produce more than console output.
Attach a small structured record to each important run:
import { test } from '@playwright/test';
test('geo test with environment record', async ({
page,
}, testInfo) => {
await page.goto('https://api.ipify.org?format=json');
const { ip } = JSON.parse(
await page.locator('body').innerText()
);
const environment = await page.evaluate(() => ({
locale: navigator.language,
languages: navigator.languages,
timezone:
Intl.DateTimeFormat().resolvedOptions().timeZone,
userAgent: navigator.userAgent,
}));
const record = {
runId: testInfo.testId,
timestamp: new Date().toISOString(),
exitIp: ip,
...environment,
};
await testInfo.attach('geo-environment', {
body: Buffer.from(
JSON.stringify(record, null, 2)
),
contentType: 'application/json',
});
});You can extend this artifact with the verified country, region, ASN, proxy-session ID, configured coordinates, browser version, and selected application responses.
Do not store passwords, authorization headers, access tokens, or unnecessary personal data in these records.
A Practical Geo-Debugging Sequence
- Record the environment reported by the affected user.
- Reproduce the flow without changing location signals.
- Start with clean cookies and browser storage.
- Change only the exit location.
- Verify the actual exit IP and how it is classified.
- Capture relevant HTTP responses and cache headers.
- Change locale independently.
- Change timezone independently.
- Test browser geolocation independently.
- Test the combined regional environment.
- Repeat through several exit IPs.
- Save traces and structured environment records.
At the end of the investigation, you want to replace a vague statement such as:
“The page is different in Germany.”
with something like:
“The pricing API returns a different product configuration when requests exit through German IP addresses. The result remains unchanged when locale, timezone, and browser coordinates are modified independently.”
That is a reproducible engineering finding.
Common Geo-Testing Mistakes
- Treating IP location as the whole environment. IP address, browser coordinates, locale, timezone, cookies, and account data are different signals.
- Changing everything at once. You may reproduce the problem without discovering which input caused it.
- Rotating IPs in the middle of a stateful journey. This can create artificial login, fraud, or checkout failures.
- Reusing dirty browser state. Old cookies or storage can make supposedly identical tests behave differently.
- Trusting the requested proxy location. Verify the visible exit IP and the geolocation database relevant to your application.
- Generalizing from one exit IP. One observation does not represent an entire city or country.
- Comparing only screenshots. Network responses often explain differences that the rendered page cannot.
- Logging too much. Network bodies, traces, and storage files can contain credentials or private data.
- Using an ambiguous baseline. Record explicit countries, locales, timezones, coordinates, and browser versions.
Final Thoughts
Reliable geo-testing is not simply about appearing to be somewhere else.
It is about turning geography into a controlled test variable and separating it from browser coordinates, locale, timezone, session state, account state, and application behavior.
Playwright provides the browser-side controls: isolated contexts, proxy configuration, geolocation emulation, network inspection, tracing, and repeatable test execution. A geo-targeted proxy supplies the network location when the application or a third-party service makes decisions based on public IP.
The most useful geo-test is not the one that proves two pages look different. It is the one that identifies the smallest environmental change that reliably causes the application to behave differently.
Once that variable is isolated and the environment is recorded, a report such as “this only happens in another country” becomes a test case another engineer can reproduce, inspect, and eventually automate.


