Testing Geo‑Targeted Feature Flags with Rotating Residential Proxies
October 1, 2026
Why Geo‑Targeted Feature Flags Need Real IPs
Feature‑flag platforms (LaunchDarkly, Unleash, Split.io, etc.) often gate functionality by the visitor’s country, region, or even city. A flag that enables a new checkout flow for Germany but hides it in France cannot be verified from a single data‑center IP. You need traffic that looks like it originates from the target locale, complete with the correct ISP ASN, residential subnet reputation, and realistic latency.
Rotating residential proxies give you exactly that: a pool of real‑user IPs spread across the globe, each request exiting from a different household connection. By routing each test request through a proxy that matches the flag’s targeting rule, you can assert that the flag evaluates to the expected variant without ever leaving your CI pipeline.
Core Challenges
| Challenge | Why It Matters | Proxy‑Based Fix |
|---|---|---|
| IP‑based targeting | Flags read the client IP (or a geo‑lookup service) to decide variant. | Choose a proxy whose exit node resolves to the desired country/region. |
| Sticky‑session requirements | Some flag SDKs cache the variant per session cookie. | Use sticky (session‑persistent) proxies for the duration of a user journey. |
| Rate limits & bot detection | Repeated requests from the same IP trigger WAFs. | Rotate IPs per request or per test case; residential pools have high reputation. |
| Latency variance | Real users experience different RTTs; performance flags may depend on it. | Measure latency per proxy and optionally filter by max RTT. |
Selecting the Right Proxy Type
| Proxy Type | Best For | Drawbacks |
|---|---|---|
| Rotating Residential | Broad geo coverage, high trust, low block rate. | Higher cost per GB; variable latency. |
| Static ISP (Sticky) | Long‑lived sessions (login, checkout) where the same IP must persist. | Limited geo granularity; fewer IPs per region. |
| Mobile | Testing carrier‑specific flags (e.g., 5G‑only features). | Smallest pools, highest price. |
Recommendation: Start with a rotating residential pool that offers country‑level targeting and session stickiness (e.g., proxy.example.com:8000?country=DE&session=12345). Most providers expose these parameters via query string or header.
Wiring Proxies Into Your Test Suite
Below is a Python example using requests and a hypothetical feature‑flag client (ldclient). The same pattern works in Node.js, Go, or any language that can set an HTTP proxy.
# test_geo_flags.py
import os
import json
import time
import requests
from ldclient import LDClient
from ldclient.config import Config
# ------------------------------------------------------------------
# 1️⃣ Proxy configuration (rotate per test case)
# ------------------------------------------------------------------
PROXY_ENDPOINT = os.getenv("ROTATING_PROXY") # e.g. http://user:pass@proxy.example.com:8000
def proxy_for_country(country_code: str) -> dict:
"""Return a proxy dict that forces the exit node to the given ISO‑3166‑1 alpha‑2."""
# Many providers accept a `country=` query param or a custom header.
# Adjust to your vendor's API.
return {
"http": f"{PROXY_ENDPOINT}?country={country_code}",
"https": f"{PROXY_ENDPOINT}?country={country_code}",
}
# ------------------------------------------------------------------
# 2️⃣ Feature‑flag client (LaunchDarkly shown; swap for your SDK)
# ------------------------------------------------------------------
ld_client = LDClient(Config(sdk_key=os.getenv("LD_SDK_KEY")))
# ------------------------------------------------------------------
# 3️⃣ Helper: evaluate a flag through a specific proxy
# ------------------------------------------------------------------
def evaluate_flag_via_proxy(flag_key: str, country: str, context: dict) -> bool:
"""Return the boolean variation for `flag_key` as seen from `country`."""
proxies = proxy_for_country(country)
# The SDK may not expose a proxy hook; we fall back to a raw HTTP call
# to the flag evaluation endpoint (most SaaS platforms provide one).
url = f"https://app.launchdarkly.com/api/v2/flags/{flag_key}/evaluate"
headers = {
"Authorization": f"Bearer {os.getenv('LD_SDK_KEY')}",
"Content-Type": "application/json",
}
payload = {
"context": context,
"environment": os.getenv("LD_ENV")
}
resp = requests.post(url, json=payload, headers=headers, proxies=proxies, timeout=10)
resp.raise_for_status()
return resp.json()["value"]
# ------------------------------------------------------------------
# 4️⃣ Test matrix – one row per targeted region
# ------------------------------------------------------------------
TEST_MATRIX = [
{"country": "DE", "expected": True, "context": {"key": "user-1", "kind": "user", "country": "DE"}},
{"country": "FR", "expected": False, "context": {"key": "user-2", "kind": "user", "country": "FR"}},
{"country": "US", "expected": True, "context": {"key": "user-3", "kind": "user", "country": "US"}},
]
def run_tests() -> None:
for case in TEST_MATRIX:
country = case["country"]
expected = case["expected"]
ctx = case["context"]
start = time.time()
try:
value = evaluate_flag_via_proxy("new-checkout-flow", country, ctx)
elapsed = time.time() - start
assert value == expected, f"{country}: got {value}, expected {expected}"
print(f"✅ {country} – {value} (latency {elapsed:.2f}s)")
except Exception as exc:
print(f"❌ {country} – ERROR: {exc}")
if __name__ == "__main__":
run_tests()
What the script does
- Builds a proxy URL per country – the provider’s
country=query param forces the exit node into the target market. - Calls the flag‑evaluation REST endpoint through that proxy. Most SaaS flag services expose an evaluation API; if yours only offers a client‑side SDK, you can spin up a tiny HTTP wrapper (Flask/FastAPI) that forwards the SDK call behind the proxy.
- Asserts the returned variation matches the rollout plan.
- Logs latency so you can spot outliers (e.g., a proxy in Germany routing through a US data center).
Sticky Sessions for Multi‑Step Journeys
Some flags depend on a session (e.g., a user logs in, then sees a personalized dashboard). Rotating the IP on every request would break the session cookie. Use a sticky proxy for the whole journey:
# sticky_proxy.py
import requests
import os
SESSION_ID = "geo-test-session-42" # any opaque string
PROXY_ENDPOINT = os.getenv("STICKY_PROXY") # e.g. http://user:pass@proxy.example.com:8000
def sticky_proxy(country: str) -> dict:
return {
"http": f"{PROXY_ENDPOINT}?country={country}&session={SESSION_ID}",
"https": f"{PROXY_ENDPOINT}?country={country}&session={SESSION_ID}",
}
# Example: login → fetch dashboard → assert flag
proxies = sticky_proxy("DE")
s = requests.Session()
s.proxies.update(proxies)
login = s.post("https://app.example.com/api/login", json={"email": "test+de@example.com", "pwd": "secret"})
login.raise_for_status()
dashboard = s.get("https://app.example.com/api/dashboard")
assert dashboard.json()["features"]["new-checkout-flow"] is True
Key point: keep the same session= token for the entire flow; the proxy provider will pin you to a single residential IP for the lifetime of that token (usually 10‑30 min).
CI/CD Integration
- Store proxy credentials in your secret manager (GitHub Actions
secrets, GitLab CI variables, CircleCI contexts). - Spin up a matrix job – one job per target country. Example for GitHub Actions:
# .github/workflows/geo-flag-test.yml
name: Geo‑Targeted Feature Flag Tests
on: [push, pull_request]
jobs:
test-flag:
runs-on: ubuntu-latest
strategy:
matrix:
country: [DE, FR, US, BR, JP]
env:
ROTATING_PROXY: ${{ secrets.ROTATING_PROXY }}
LD_SDK_KEY: ${{ secrets.LD_SDK_KEY }}
LD_ENV: production
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install deps
run: pip install -r requirements.txt
- name: Run test for ${{ matrix.country }}
run: |
python - <<'PY'
import os, sys
sys.path.insert(0, ".")
from test_geo_flags import evaluate_flag_via_proxy
ctx = {"key": "ci-user", "kind": "user", "country": "${{ matrix.country }}"}
val = evaluate_flag_via_proxy("new-checkout-flow", "${{ matrix.country }}", ctx)
expected = {"DE": True, "FR": False, "US": True, "BR": False, "JP": True}["${{ matrix.country }}"]
assert val == expected, f"{matrix.country}: {val} != {expected}"
print(f"✅ ${{ matrix.country }} -> {val}")
PY
The matrix runs in parallel, each job grabbing a fresh residential IP for its country. If a job flakes due to a bad proxy, the failure is isolated and you can add a retry step.
Verifying Results Beyond Boolean Flags
Flags often return JSON payloads (e.g., { "variant": "beta", "config": { "maxItems": 10 } }). Extend the assertion logic:
def evaluate_json_flag(flag_key: str, country: str, context: dict) -> dict:
proxies = proxy_for_country(country)
url = f"https://app.launchdarkly.com/api/v2/flags/{flag_key}/evaluate"
resp = requests.post(url, json={"context": context, "environment": os.getenv("LD_ENV")},
headers={"Authorization": f"Bearer {os.getenv('LD_SDK_KEY')}"},
proxies=proxies, timeout=10)
resp.raise_for_status()
return resp.json() # full payload
# In test:
payload = evaluate_json_flag("pricing-tiers", "BR", ctx)
assert payload["variant"] == "premium"
assert payload["config"]["currency"] == "BRL"
Troubleshooting Checklist
| Symptom | Likely Cause | Fix |
|---|---|---|
| 403/429 from flag service | Proxy IP reputation low or rate‑limited. | Switch to a higher‑quality residential pool; add time.sleep(1) between requests. |
| Flag always returns default | Proxy not actually exiting in target country. | Verify with https://ipinfo.io/json through the same proxy; adjust provider’s country= param. |
| Session cookie lost | Rotating proxy used where sticky required. | Use sticky proxy endpoint (session=) for the whole flow. |
| High latency > 2 s | Proxy node overloaded or far from flag edge. | Filter proxies by max_latency_ms if provider supports it; fallback to static ISP for that region. |
| TLS handshake failure | Proxy requires SNI/ALPN that your HTTP client doesn’t send. | Ensure requests/httpx uses system CA bundle; for Node use tls.checkServerIdentity = () => undefined only in test. |
Extending the Pattern
- Feature‑flag canary analysis – run the same matrix before/after a rollout and diff the variant distribution.
- A/B test validation – combine with an analytics event collector (e.g., Segment) proxied through the same IPs to confirm exposure.
- Compliance checks – prove to auditors that a GDPR‑restricted feature never evaluates to
truefor EU IPs.
TL;DR Checklist for Your Next Geo‑Flag Test Run
- Pick a rotating residential provider with per‑country targeting and optional sticky sessions.
- Create a small wrapper (Python/Node/Go) that injects the proxy per test case.
- Call the flag evaluation API (or an internal proxy‑aware SDK shim) through that wrapper.
- Assert the returned variant matches the rollout spec for each country.
- Run in CI as a matrix – one job per region – storing proxy secrets safely.
- Monitor latency & error rates; swap out bad proxies automatically.
- Document the matrix in your feature‑flag rollout ticket so QA and product can audit the coverage.
By treating the proxy as a first‑class test dependency rather than an after‑thought, you get deterministic, repeatable proof that your geo‑targeted flags behave exactly as designed—no more “it works on my machine” surprises.