Proxy Showing Wrong Country? Align Every Geo Signal
Proxy showing wrong country content even though the IP checks out? Test cookies, Accept-Language, timezone, CDN edge and geo-IP lag in order, then fix each one.
A proxy showing wrong country content is usually not a proxy fault at all. Your IP checker says Germany, the exit really is German, and the shop still greets you in English with prices in dollars. That happens because a website decides where you are from several inputs, and the IP is only one of them. This guide walks through each of the other inputs in the order that costs least to test, gives you a check for every one, and shows how to make all of them agree with the proxy country.
What You See When the Signals Disagree
The reports we get from customers almost always take one of these shapes:
- "My IP checker says Frankfurt, but the store shows USD and a US delivery banner."
- "I switched my proxy from the US to Japan and the site still shows US prices."
- "Search results are in the right country but the language is English."
- "Some requests come back German, some come back American, with the same exit country."
- "The country is right in curl and wrong in my headless browser."
Each phrase points somewhere different. The second one is nearly always a cookie. The third is almost always a missing or wrong Accept-Language header. The fifth points at the browser's locale and timezone. The fourth is the interesting one, and it usually means some of your exits are mapped to a different country in the database the site licenses, or a CDN cache is serving one country's page to everyone.
Write down which shape you have before you start. It tells you which step below will most likely end the search.
Six Inputs a Site Uses to Pick Your Country
A proxy changes exactly one of these: the IP address the site sees. The rest come from your client, your history with the site, or the site's own infrastructure.
| Input | Where it comes from | Quick check | Can the proxy fix it? |
|---|---|---|---|
| Geo-IP lookup of the exit | The site's or CDN's geo database | `/cdn-cgi/trace` `loc=` on Cloudflare sites | Yes, with a different exit |
| Stored cookie or saved region | A previous visit, a persistent browser profile | Retry with an empty cookie jar | No. Clear state on your side |
| `Accept-Language` header | Your HTTP client or browser locale | Echo your own request headers | No. Set the header |
| Browser locale and timezone | The machine running the browser | Read `navigator.language` and `Intl` timezone | No. Set them in the browser context |
| CDN edge chosen by DNS | Whoever resolved the hostname | Compare the connected IP across exits | Partly. Resolve at the proxy |
| Account, cache or app logic | The site itself | Compare logged out, cache headers | No |
Two columns matter here. The quick checks are what the steps below expand on. The last column is the honest part: a different proxy can only repair the first row and part of the fifth. If the cause sits in any other row, buying exits in more countries won't change the page. For how the IP row works under the hood, what geo-targeting means in proxies covers the product side.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Step 1: Ask the Site's Edge Which Country It Sees
Before touching headers, find out whether the site agrees with your IP checker. Many sites sit behind Cloudflare, and Cloudflare documents a troubleshooting endpoint at https:// (Cloudflare docs, "cdn-cgi endpoint", read 2026-10-30). Request it on the target's own hostname, through the proxy:
curl -s -x "$PROXY_URL" https://www.sparkproxy.io/cdn-cgi/trace | grep -E "^(ip|loc|colo)="
Run directly from our test machine on 2026-10-30, it returned:
ip=49.37.250.31
colo=CDG
loc=IN
Read loc, not colo. loc is the country Cloudflare's edge assigned to the connecting IP (our Indian address read IN). Cloudflare can hand its country lookup to the site's origin in the CF-IPCountry header, documented in its HTTP headers reference, so loc is a close stand-in for what the site's code receives. The trace fields aren't documented one by one, so treat loc as a strong hint rather than a contract. colo is just the data center that answered. Our Indian IP was served from Paris (CDG), and nobody's content should depend on that. Through a proxy, ip should show the exit address and loc its country.
This one request splits the problem in two:
locis wrong. The site's edge really thinks the exit is elsewhere. Go to step 6 (database lag), and try a few other exits from the same country.locis right, the page is still wrong. The IP is fine. Something in your client or your history is overriding it, so work through steps 2 to 5.
Sites that aren't on Cloudflare don't expose this endpoint. On those, look for the site's own region marker in the HTML (a country selector, a currency code, a lang attribute) and use that as your reading instead.
Step 3: Send an Accept-Language That Matches the Exit
When the country is right but the language is wrong, check what your client sends. We ran both common command-line clients against a local header echo server on 2026-10-30: Python Requests 2.32.5 sent User-Agent, Accept-Encoding, Accept and Connection, and no Accept-Language at all. curl 8.15.0 didn't send one either.
A site that gets no language preference has to guess. Many fall back to their default language, which on international sites is often English, so a perfectly German IP still gets the English page. A browser does send the header, but it builds it from the machine's locale. Headless Chromium on our en-US Windows server sent en-US,en;q=0.9 regardless of which proxy it used.
The fix is a header that matches the exit country:
import os
import requests
PROXY = os.environ["PROXY_URL"]
HEADERS = {
"Accept-Language": "de-DE,de;q=0.9,en;q=0.5",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
}
with requests.Session() as s:
s.headers.update(HEADERS)
r = s.get(
"https://shop.sparkproxy.io/",
proxies={"http": PROXY, "https": PROXY},
timeout=30,
)
print(r.status_code, r.headers.get("Content-Language"), r.url)
Watch Content-Language and the final URL. Sites that localize by header often redirect to a path such as /de/ or answer with Content-Language: de. If both stay English with a German header and a German loc, the site probably decides language from an account setting or a cookie, which takes you back to step 2.
Keep the header plausible. A q= list with the local language first and English as a low-weight fallback is what multilingual visitors send; a lone fr on a request from a Brazilian exit is a mismatch some bot filters notice.
Step 4: Align Browser Locale and Timezone
Browsers expose two more location hints to the page's JavaScript: the language list in navigator.language and the timezone from Intl.DateTimeFormat().resolvedOptions().timeZone. Both come from the operating system running the browser, not from the network. Playwright's documentation says the context option locale "will affect navigator.language value, Accept-Language request header value as well as number and date formatting rules", and that timezone_id "changes the timezone of the context". Both default to the system settings.
We measured what that means in practice with Playwright for Python 1.63.0 driving Chromium 151.0.7922.34 on a server set to India Standard Time:
| Context options | `Accept-Language` sent | `navigator.language` | Timezone seen by the page |
|---|---|---|---|
| none | `en-US,en;q=0.9` | `en-US` | `Asia/Calcutta` |
| `locale="de-DE"`, `timezone_id="Europe/Berlin"` | `de-DE` | `de-DE` | `Europe/Berlin` |
| `extra_http_headers` only, German list | `de-DE,de;q=0.9,en;q=0.5` | `en-US` | system default |
| `locale="de-DE"` plus `extra_http_headers` | `de-DE` | `de-DE` | as set |
The third row is the trap we didn't expect. Setting only the header fixes what the server sees, but the page's own scripts still read en-US, so a site that localizes in JavaScript (or a fraud script comparing the two) sees an English browser on a German IP. The fourth row shows that when both are set, the locale value won on the navigation request in our test. Set locale and timezone_id together and treat them as the source of truth.
A site can also ask for physical position through the Geolocation API, which reports the device's real location if permission is granted. That's a separate signal again, and it's covered in the limitations section of the geo-targeting guide. WebRTC can expose a non-proxy address in browsers too; see WebRTC leaks if your checks show a second IP.
These map well for the common targets. For countries spanning several zones, match the zone to the exit city:
| Exit country | `locale` | `timezone_id` |
|---|---|---|
| Germany | `de-DE` | `Europe/Berlin` |
| France | `fr-FR` | `Europe/Paris` |
| United Kingdom | `en-GB` | `Europe/London` |
| Japan | `ja-JP` | `Asia/Tokyo` |
| Brazil | `pt-BR` | `America/Sao_Paulo` |
| United States (East) | `en-US` | `America/New_York` |
| United States (West) | `en-US` | `America/Los_Angeles` |
Step 5: Check Which CDN Edge DNS Sent You To
Large CDNs answer DNS with an edge near whoever asked. If your machine resolves the hostname and then sends the resulting IP through a foreign proxy, you reach an edge picked for your location, not the exit's. The full mechanism, including EDNS Client Subnet and the centralised-resolver case, is in how proxies handle DNS resolution; here's the check.
Compare the address you actually connected to across two exit countries:
for P in "$PROXY_DE" "$PROXY_JP"; do
curl -s -o /dev/null -x "$P" -w "%{remote_ip} %{http_code}\n" https://shop.sparkproxy.io/
done
Behind an HTTP proxy, remote_ip is the proxy itself, so this check is most useful with SOCKS. Run it with socks5:// and then socks5h:// URLs: with socks5:// your machine resolves the name, with socks5h:// the proxy does. If the page only goes wrong with socks5://, switch the scheme and you're done. If the edge is identical from a German and a Japanese exit even with socks5h://, resolution isn't following your exit, and that's a question for the provider's resolver setup. Edge choice usually affects latency more than content, so treat this as a cause only when the other steps are clean.
Step 6: When the Geo-IP Database Is Behind
If step 1 showed the wrong loc, the site's geo data disagrees with your IP checker. That isn't surprising: they're often different databases, refreshed on different schedules.
Two published cadences show where the lag comes from. MaxMind's support article on database updates (read 2026-10-30) lists GeoIP Country as updated every weekday and the free GeoLite Country every Tuesday and Friday. Cloudflare's IP geolocation docs (read 2026-10-30) say it updates its database "multiple times per week" and that confirmed corrections "generally take effect within a few business days", adding that it "cannot guarantee that its IP geolocation will align with other providers". Those are the vendor's side. The site's side adds its own delay: MaxMind's own example geoipupdate crontab runs twice a week, and a site that downloads a file once and never updates keeps an old picture for months.
So an IP range that moved countries recently can be correct in one checker and wrong at the site for days or longer. What you can do:
- Sample more exits. Request
/cdn-cgi/tracethrough ten fresh connections from the same country and count thelocvalues. If only a few exits are off, drop them and move on. - Report the range. Cloudflare and MaxMind both run correction forms; give the provider the exit IPs and the
locyou saw so they can push a fix through their geofeed. - Don't trust one checker. How to test across registration data, commercial databases and the target's own view is laid out in validating IP geolocation accuracy.
This is the one cause in the guide that's properly the proxy's to fix, and the fix is a different IP, not a different setting.
Causes No Proxy Can Fix
Some wrong-country pages are decided after the IP has been read correctly. No exit change helps with these:
- A shared CDN cache. If the site caches pages without keying them by country, the first visitor's version gets served to everyone. Look at the response headers:
CF-Cache-Status: HITor a largeAgevalue on a page that should vary by country, and noVaryheader naming a country or language input, point at the cache. Adding a harmless query string such as?nocache=1often bypasses it; if that alone changes the country, you've confirmed it. - Account region. Logged-in sessions usually follow the account's billing or shipping country. Test logged out.
- Payment and licensing rules. Store catalogs and streaming libraries can be tied to the payment method's country, not the visitor's location.
- App logic that pins a region. Some sites redirect to a regional subdomain once and then stop checking.
A Playwright Client With Every Signal Aligned
Putting steps 1 to 4 together: one context per country, an empty cookie jar, matching locale and timezone, and a loc check before the real page.
import os
from playwright.sync_api import sync_playwright
PROFILES = {
"DE": {"locale": "de-DE", "timezone_id": "Europe/Berlin"},
"JP": {"locale": "ja-JP", "timezone_id": "Asia/Tokyo"},
}
def edge_country(page, host):
page.goto("https://" + host + "/cdn-cgi/trace")
text = page.inner_text("body")
fields = dict(line.split("=", 1) for line in text.splitlines() if "=" in line)
return fields.get("loc"), fields.get("ip")
def scrape(country, url, host):
proxy = {
"server": os.environ["PROXY_SERVER_" + country], # e.g. http://gateway.sparkproxy.io:11000
"username": os.environ["PROXY_USER_" + country],
"password": os.environ["PROXY_PASS"],
}
with sync_playwright() as p:
browser = p.chromium.launch(proxy=proxy)
context = browser.new_context(**PROFILES[country]) # fresh cookies every run
page = context.new_page()
loc, ip = edge_country(page, host)
if loc != country:
browser.close()
raise RuntimeError("exit {} geolocates to {}, not {}".format(ip, loc, country))
page.goto(url, wait_until="domcontentloaded")
signals = page.evaluate(
"() => [navigator.language, Intl.DateTimeFormat().resolvedOptions().timeZone]"
)
html = page.content()
browser.close()
return signals, html
signals, html = scrape("DE", "https://shop.sparkproxy.io/", "shop.sparkproxy.io")
print(signals, len(html))
The edge_country check only works on Cloudflare-fronted hosts; for others, replace it with a marker from the site's own HTML. Raising on a mismatch is deliberate. Retrying through a fresh connection gets another exit under Random Rotation, which is the cheap fix for step 6, and SparkProxy plans include unlimited bandwidth (plans start from $75/month in USD, per sparkproxy.io in October 2026), so a discarded check costs concurrency time rather than traffic.
Country-Correct Fetches With the Scraping API
If you'd rather not manage browsers, the SparkProxy Scraping API takes the country and the language header as parameters. Per the Scraping API docs read 2026-10-30: base https://scrape.sparkproxy.io/api/v1, header X-API-Key, country_code takes an ISO 3166-1 alpha-2 code and adds 5 credits, and forward_headers is a JSON object merged with the browser defaults, where any key you set overrides the default. render_js defaults to true (5 credits); render_js=false is a plain fetch at 1 credit. The docs also say session_id "does not persist cookies across requests", so each call starts without stored region cookies, which removes step 2 entirely.
import base64
import json
import os
import requests
API = "https://scrape.sparkproxy.io/api/v1"
HEADERS = {"X-API-Key": os.environ["SPARKPROXY_API_KEY"]}
def fetch_as(country, lang, url, render=False):
params = {
"url": url,
"country_code": country,
"forward_headers": json.dumps({"Accept-Language": lang}),
"render_js": "true" if render else "false",
"json_response": "true",
}
r = requests.get(API, params=params, headers=HEADERS, timeout=120)
r.raise_for_status()
env = r.json()
body = base64.b64decode(env.get("body") or "").decode("utf-8", "replace")
return env.get("status_code"), env.get("credits_used"), body
status, credits, body = fetch_as("DE", "de-DE,de;q=0.9,en;q=0.5", "https://shop.sparkproxy.io/")
print(status, credits, "EUR" in body)
json_response=true is what returns the envelope (job_id, status_code, duration_ms, credits_used, body), and body is base64, so the code decodes it before looking for the currency marker. Don't confuse that with format=json, which converts page content and doesn't produce the envelope. The docs list no timezone or locale parameter today, so when a target localizes in JavaScript, render with render_js=true and check the result rather than assuming the browser's timezone matches the country. We couldn't capture a live response for this guide (the API edge returned a Cloudflare 521 on 2026-10-30), so the envelope fields above come from the documentation.
Frequently asked questions
FAQ
Because the site also reads stored cookies, your Accept-Language header, the browser's locale and timezone, and its own geo database. Any one of those can override a correct IP, and a stored region cookie from an earlier visit is the most common culprit.
On Cloudflare-fronted sites, request https:// through the proxy and read the loc= line, which is the country Cloudflare assigned to your exit. On other sites, use the site's own currency, language or region selector as the reading.
Often for language, sometimes for country. Python Requests and curl send no Accept-Language by default, so many sites fall back to English; set a header that matches the exit, such as de-DE,de;q=0.9,en;q=0.5 for Germany.
Pass locale and timezone_id to new_context(), for example locale="de-DE" and timezone_id="Europe/Berlin". In our test, setting only an Accept-Language header left navigator.language at the system value, so set both options rather than the header alone.
Yes. MaxMind publishes its country data on a weekday or twice-weekly schedule and Cloudflare says corrections take a few business days, and sites that rarely download updates lag further. Try other exits from the same country and report the bad range.
Only when the site's geo lookup is wrong about the exit. If the edge reports the right country, the cause is cookies, headers, locale, caching or your account region, and a new IP won't change the page.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles
Scrapy: Could Not Open CONNECT Tunnel With Proxy (Fixed)
Scrapy could not open CONNECT tunnel with proxy? Read the status inside the TunnelError, then fix 407s, dropped Proxy-Authorization, timeouts and concurrency.
Proxychains Configuration: Force Any Linux Tool via Proxy
Proxychains configuration for proxychains-ng 4.17: strict vs dynamic chain, proxy_dns, auth lines, Go and static binary workarounds, and a DNS leak test.
aiohttp ClientProxyConnectionError: Fix Every Proxy Error
Fix aiohttp ClientProxyConnectionError, ClientHttpProxyError 407 and ServerDisconnectedError with proxies: verbatim errors, a diagnosis order and tested fixes.
