Guide

Geo-Testing Your App With Proxies: A QA Playbook

Localised pricing and regional features ship untested. How to verify what each country sees, in CI.

HProxy Team··9 min read
HProxy.Guide

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.44/GB, pay as you go.

See plans & pricing

Almost every product that ships to more than one country has a class of bug nobody on the team has ever seen: the version other countries get. Prices convert wrong. A catalogue hides items it should show. A cookie banner that is mandatory in one jurisdiction fails to appear. A payment method that only exists in one market is offered everywhere, or nowhere.

None of it shows up in review, because review happens from one office on one connection, and from there the application looks correct. The first report usually arrives from a customer, in a language the support inbox has to translate, describing a page nobody can reproduce.

The fix is unglamorous: make the request from where the customer is, and assert on what comes back. That is a proxy, one line of configuration, and a small amount of discipline about what you assert on.

What actually varies by country

Worth listing, because teams routinely test the first item and none of the rest.

Currency and price. The obvious one. It is also the one with a second failure mode nobody checks: the price is correct but the tax display, rounding or currency symbol placement is wrong for that locale.

Catalogue and availability. Regional licensing, shipping restrictions and staged rollouts all change what a visitor can see. A product that is hidden in a market it should sell in is invisible revenue, and no error is logged anywhere.

Language and locale. Which language the page picks, and how it decides. Some applications choose by IP, some by browser preference, some by an account setting, and the interesting bugs live where two of those disagree.

Legal and consent. Cookie banners, privacy notices, statutory disclosures and age gates are jurisdictional. Getting one wrong is a compliance problem rather than a cosmetic one, and it is entirely invisible from the wrong country.

Payment methods. Which options appear at checkout is regional, and a checkout offering a method the customer cannot use converts worse than one that never mentions it.

Third-party behaviour. Your CDN, your consent platform, your analytics and your payment provider all behave regionally, and their behaviour changes without a release on your side.

That last one is the argument for running this continuously rather than once. Your own code can be stable while what a country sees changes underneath it.

The setup, at three levels of effort

Three levels, in the order teams actually adopt them
  1. One-off look

    a proxied browser session, five minutes

  2. Scripted check

    a script over N countries, run before release

  3. CI matrix

    one job per market, failing on a real regression

Source: HProxy

Level one, the one-off look. Point a browser through an exit in the country and open the page. Enough to answer "does Germany see the right price". Not enough to know it stays right.

Level two, the scripted check. A short script that loops over your markets, requests the same handful of URLs through an exit in each, and prints what it found. Run it before a release. This catches most of what matters and takes an afternoon.

# The whole idea, in the smallest possible form.
for CC in us gb de fr jp br; do
  printf '%s: ' "$CC"
  curl -s -x "http://USER-country-$CC:PASS@HOST:PORT" \
       https://your-app.example/pricing \
    | grep -oE '(EUR|USD|GBP|JPY|BRL)[^<]{0,12}' | head -1
done

The exact way a country is selected differs by provider, so read your own dashboard for the parameter format. The shape is what matters: one request per market, one assertion per response.

Level three, CI. The same checks as a matrix job, one entry per supported market, running on every deploy. A regional break then arrives as a failing test with a country name on it, which is a completely different experience from a support ticket three weeks later.

For browser-level testing, the proxy attaches per context rather than per machine, which is exactly what makes parallel country coverage practical. Our Playwright and Selenium guides cover the configuration in each.

Assert on the right things

This is where geo-testing suites go wrong, and it has nothing to do with proxies.

Do not assert on screenshots. Visual diffs across locales fail constantly for reasons nobody cares about: a longer German word wraps, a font renders differently, a promotional banner rotates. You will disable the suite within a month.

Do assert on the specific facts that must be true. The currency code present in the page. The tax line existing where it is legally required. A product id appearing in the catalogue for the markets licensed to sell it. The lang attribute matching the market. The consent banner rendering where it is mandatory. Each of these is a one-line assertion and each one fails for exactly one reason.

Assert on the negative cases too. A payment method that must not appear in a market is as important as one that must. Those are the assertions that catch a misconfigured rollout.

Record what the exit actually was. Log the exit IP and its resolved country alongside each result. When a test fails, the first question is always whether the request really came from where you asked, and without that line you cannot answer it.

The CI shape

A loop over countries is the whole implementation. Written as a script rather than as one CI vendor's YAML, because the interesting decisions are not in the config format and this way it runs the same on a laptop.

#!/usr/bin/env bash
# geo-check.sh: one exit per market, one line of result per market.
# Credentials come from the environment, never from the file.
set -uo pipefail                      # deliberately NOT -e: see below

MARKETS=(us gb de fr jp br au ca)
FAILED=0

for CC in "${MARKETS[@]}"; do
  RESULT=$(COUNTRY="$CC" node ./test/geo-check.mjs 2>&1)
  STATUS=$?
  case $STATUS in
    0) printf '  ok    %s  %s\n' "$CC" "$RESULT" ;;
    2) printf '  skip  %s  no exit available in this market\n' "$CC" ;;
    *) printf '  FAIL  %s  %s\n' "$CC" "$RESULT"; FAILED=1 ;;
  esac
done

exit $FAILED

Three details in there matter more than the rest, and they are the ones people leave out.

Every market runs even after one fails. The obvious set -e, or a CI matrix left on fail-fast, cancels the remaining markets the moment one breaks, so you learn about one broken country instead of five. For this kind of test that is exactly backwards: the whole point is a complete picture per run.

A missing exit is a third outcome, not a failure. Exit status 2 above means the harness could not obtain an address in that country, which is a different fact from the page being wrong. Collapsing the two teaches the team to ignore red.

Credentials come from the environment. The country belongs in the market list and the secrets belong in your CI's secret store, so adding a market stays a one-line change that anyone can review.

Run it on deploy rather than on every commit. Regional behaviour changes when something ships or when a third party changes underneath you, not when someone edits a README, and a suite that runs constantly gets muted the first time a provider has a bad afternoon.

Keeping it deterministic enough to trust

The reason geo-suites get disabled is flakiness, and most of it is self-inflicted.

Retry the network, not the assertion. A request that fails to reach the exit is infrastructure noise and should be retried transparently. An assertion that fails is a finding and must never be retried away. Conflating the two is how a suite ends up green while the German price is wrong.

Pin what you can, and accept what you cannot. Currency codes, legal text and catalogue membership are stable enough to assert on directly. Exact prices move with exchange rates and promotions, so assert on the shape rather than the value: a currency symbol matching the market, a number within a plausible range, a tax line present. Asserting on €49.99 guarantees a false failure the week marketing runs a sale.

Treat an unavailable exit as a skip, not a failure. Availability in a small market varies, and a job that reports "could not obtain an exit in this country" is telling you something different from "the page was wrong". Report them separately or you will stop believing the red.

Keep one canary that must always pass. A single check against your own domain from your own network, with no proxy at all, in the same job. When everything fails at once, that line tells you instantly whether the application broke or the test harness did.

The trap that invalidates the whole exercise

A geo-test is only as good as the geography of the address you used, and this is the part that quietly produces false confidence.

IP geolocation is a database, not a fact. Providers publish a country per address and the databases disagree with each other, particularly on mobile ranges and on recently reassigned blocks. If your provider says an address is in Spain and the geolocation service your application uses says it is in Portugal, your test passes against a reality your customers do not live in.

Two defences. First, verify the exit's country against the same source your application uses, not against your provider's label, and log both. Second, when a market matters commercially, use more than one address in it, because a single mislabelled address is indistinguishable from a working test.

The related trap is network type. Many applications and most third-party fraud, consent and personalisation services behave differently for a datacenter address than for a consumer one. Testing from a datacenter IP in Germany can verify a German experience no German customer will ever get. If your product treats visitors differently by network type, and most consumer products do through something in their stack, test from residential addresses. Our ASN targeting guide covers why the network behind an address is read before anything else about it.

Picking the markets, and the granularity

Test where being wrong costs something, which is a much shorter list than every country you serve.

Start with markets that have their own prices, their own legal text or their own catalogue, because those have real divergence to get wrong. Add the ones that generate the most support tickets, since that is evidence of divergence you have not found yet. Add any market where a regulator, rather than a customer, notices a mistake.

Country granularity covers language, currency and country gating. Move to city or region only where the product genuinely varies inside a country: local search results, delivery windows, store availability, regionally regulated pricing. Finer targeting costs more and constrains availability, so buy it where a measurement forces you to rather than by default.

What this costs, roughly

The reason this does not get built is usually an assumption that it is expensive, and the arithmetic rarely supports that.

A geo-test is a handful of small page requests per market per run. Even a large matrix, a dozen markets against a dozen URLs, several times a day, moves a small amount of traffic compared with any scraping workload, because you are checking pages rather than harvesting them. On a per-gigabyte product that is a rounding error against the cost of one regional pricing bug reaching production.

Our residential proxies start at $0.50/GB for a single gigabyte with no subscription and a balance that does not expire, which suits this shape of work well: usage is bursty, tied to releases, and a balance bought once carries across the quiet weeks. Before wiring anything up, run an exit through the proxy checker to see the country, the network and the anonymity grade your test will actually be presenting, because that is the variable the whole exercise depends on.

Frequently asked questions

How do I test my site from another country?
Route the request through an exit in that country and assert on what comes back. For a quick look, a single proxied browser session is enough. For anything that must keep working, put the same check in CI with one exit per market you support, so a regional break is caught by a failing test rather than by a customer.
Why not just use a VPN for geo-testing?
A VPN moves the whole machine, which makes parallel testing of several countries awkward and automation worse, since your test runner and its reporting move too. Proxies attach per request or per browser context, so a suite can check twelve markets concurrently from one machine without any of them interfering.
What kind of proxy should QA use?
Residential for anything where the site treats visitors differently by network type, which includes most consumer products, and datacenter where you only need the geography and the target does not care. If a page renders one way for a home connection and another way for a server, testing from a datacenter address verifies a version your customers never see.
Can geo-testing run in CI?
Yes, and that is where it belongs. The proxy is one configuration line in your HTTP client or browser automation, so a matrix job over your supported countries is a small change. The harder part is writing assertions that fail on the right things, which means asserting on currency, language, catalogue and legal notices rather than on a screenshot.
How many countries should we test?
Start with every market where you show different prices, different legal text or a different catalogue, since those are the ones where being wrong has a cost. Add the countries that generate the most support tickets. Testing all 195 is a way of testing none of them well.
Do I need city-level targeting?
Only when the thing you are testing varies inside a country. Local search, delivery availability, store pickup and regulated regional pricing all do. For language, currency and country gating, country-level targeting is enough and costs less.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires. $0.44/GB is the 2,000 GB+ rate; a single gigabyte is $0.50/GB, with no minimum order.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup