You set a German exit, the address geolocates correctly, and the target still treats the session as suspicious or serves you content meant for somewhere else. The IP was right. Everything else about the request was still describing a visitor in another country.
Location is not one signal. It is a cluster of them, and a real visitor produces them all from the same place because they come from one machine in one country. A scraper assembles them from different sources: the country comes from a proxy parameter, the timezone from the server’s clock, the locale from a library default, and the language header from whatever was hardcoded. When those disagree, the contradiction is more detectable than any single value, and it can also change what you collect.
The signals that have to agree
Six things tell a site where you are, and they are worth listing explicitly because most scrapers control one or two.
The IP address is the primary signal and the one your proxy sets. Accept-Language is the HTTP header expressing language preference, and many sites serve content from it directly. Timezone is observable in a browser through the JavaScript date API and, more precisely, through the Intl API’s resolved time zone. Locale covers navigator.language and the formatting conventions the Intl API resolves, which determine how dates, numbers and currency render. Currency and units, where a site lets the client hint at them. And DNS resolution, which sounds unrelated but is not: if your client resolves hostnames locally while exiting remotely, the resolution happens from your location and can hand you a regionally wrong endpoint, which is the DNS leak problem.
For plain HTTP work only the first two and DNS are visible, which is why the headers article treats Accept-Language as the main pairing. For browser automation all six are observable, and that is where mismatches usually appear.
Derive everything from one source of truth
The fix is structural rather than a checklist. If the country is chosen in one place and the timezone in another, they will drift the first time someone adds a market. Define each market once, with every signal it implies, and derive the whole session from that record.
MARKETS = {
"de": {"lang": "de-DE,de;q=0.9,en;q=0.8", "locale": "de-DE",
"tz": "Europe/Berlin", "currency": "EUR"},
"us": {"lang": "en-US,en;q=0.9", "locale": "en-US",
"tz": "America/New_York", "currency": "USD"},
"jp": {"lang": "ja-JP,ja;q=0.9,en;q=0.8", "locale": "ja-JP",
"tz": "Asia/Tokyo", "currency": "JPY"},
"br": {"lang": "pt-BR,pt;q=0.9,en;q=0.8", "locale": "pt-BR",
"tz": "America/Sao_Paulo", "currency": "BRL"},
}
def proxy_for(country, session=None):
user = f"customer-USERNAME-country-{country}"
if session:
user += f"-sid-{session}-ttl-600"
url = f"http://{user}:PASSWORD@p.shifter.io:443"
return {"http": url, "https": url}
Now a market is a single argument, and there is no code path where the country and the timezone can disagree, because nobody sets them separately.
Applying it to a browser session
Browser automation frameworks expose timezone and locale as context options, which is the correct place to set them: they apply before any page script runs, so the page cannot observe the machine’s real values.
# Playwright: proxy, timezone and locale all derived from one market record
m = MARKETS[country]
context = browser.new_context(
proxy={"server": "http://p.shifter.io:443",
"username": f"customer-USERNAME-country-{country}-sid-{sid}",
"password": "PASSWORD"},
locale=m["locale"], # navigator.language and Intl formatting
timezone_id=m["tz"], # Intl resolved time zone and Date offset
extra_http_headers={"Accept-Language": m["lang"]},
)
Setting these at context level rather than by patching properties afterwards matters, because a patched Intl.DateTimeFormat that disagrees with the Date offset is itself a detectable inconsistency, and detection scripts routinely cross-check the two.
Precision, and how much of it you need
Country granularity is enough for most work, since timezone and locale are largely national. Three cases need more care.
Countries with multiple timezones make a national default wrong for some of the population. A US exit is plausible in any of several zones, so if you are working at city-level targeting you should derive the timezone from the city rather than the country, and if you are not, pick the zone matching the largest share of users and stay consistent rather than randomising.
Countries with multiple official languages need a deliberate choice: a Swiss or Canadian exit can plausibly be several locales, and the right answer is usually whichever matches the content you are collecting, held constant.
Regional formatting differences are subtler and rarely worth chasing, but if you present a locale, let the Intl API do the formatting rather than hand-rolling date and number formats that may not match what that locale actually produces.
Consistency over the life of a session
A session is a story, and the story must not change halfway through. If a sticky session holds one address for a multi-step flow, every request in that flow must carry the same language, timezone and locale. Changing any of them mid-flow describes a visitor who moved countries between clicking search and viewing a result, which is a stronger anomaly than any static mismatch.
This is where deriving from one market record pays off again: the session identifier and the locale bundle come from the same place and last for the same duration. Bind them together explicitly, so releasing the session releases the whole identity, and a new session starts a fresh, internally consistent one. The same logic governs pairing device fingerprints with network identity in antidetect browsers.
Verifying you got it right
Assume nothing and check the two levels.
First, what the browser reports about itself. Run a page that reads the resolved timezone, navigator.language, and a formatted date, and confirm they match the market you intended. This catches configuration mistakes immediately.
JSON.stringify({
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
lang: navigator.language,
langs: navigator.languages,
offset: new Date().getTimezoneOffset(),
})
Second, and more meaningful, what the target does. Fetch a geo-sensitive page and confirm the currency, language, and regional content are what a local visitor would see. The site’s own behaviour is the real verdict, since geolocation databases and a target’s opinion do not always agree, and that is what testing location accuracy is for. If the address geolocates correctly but the content is wrong, suspect DNS resolution before anything else.
The bottom line
Location is a cluster of signals, and a site reads them together. Setting a country on the proxy while leaving timezone, locale and language at whatever your server defaults to produces a visitor who cannot exist, which is both a detection signal and a source of quietly wrong data. Define each market once with every signal it implies, derive the proxy parameters and the browser context from that one record so they cannot drift, set timezone and locale at context level rather than patching them after load, hold the whole bundle constant for the life of a session, and verify at both levels, what the browser reports and what the target actually serves. Then the only thing your traffic says about its location is the one thing you chose.
The geography itself comes from residential proxies, real home-grade addresses with country and city targeting, so the location your session claims is a location you are genuinely exiting from, with per-GB pricing that suits running the same job across many markets.