By Lucas Yoris · Performate
Geo-Distributed Load Testing: DNS, Latency, and Realistic User Journeys
Run generators close to users—or emulate latency—and avoidDNS shortcuts that fake performance.
Your k6 runner sits in us-east-1, your users open the app in São Paulo and Madrid, and the dashboard shows green p95—until Black Friday exposes cross-region database hops and a CDN cache key that only warms in Virginia. Geo distributed load testing is how you prove latency and routing match customer reality instead of your CI network shortcut.
In this guide you will learn why single-region generators lie, how to tag k6 runs by region, when to deploy distributed agents vs emulate RTT, and which DNS choices belong in the scenario—not in assumptions.
Why geography changes performance—not just ping charts
Aggregating all virtual users through one regional generator hides problems that only appear on the wire path customers use:
- GeoDNS steers names to different anycast edges; wrong resolver in the test means wrong POP.
- TLS and HTTP/3 negotiation differs by distance; CPU on the client side is not the story—RTT amplifies tail latency on chatty APIs.
- CDN cache hit ratio varies by region; a warm cache in Virginia does not help Chile on first byte.
- Database followers may sit in one region while writes stay primary elsewhere—read-heavy journeys look fast only near the writer.
- Compliance routing forces EU traffic through EU clusters; a US-only generator never exercises that path.
Think of performance as a map, not a single number. One dot on the map is a lab result; a contour plot is what your SLO should defend.
Pair regional tags with k6 observability metrics and tags so merged summaries still split http_req_duration by region:eu-west vs region:sa-east.
Practical k6 implementation: regional tags and parallel scenarios
You can run distributed k6 (k6 Cloud or self-hosted runners per distributed execution) or run one script with explicit region tags per scenario when each process executes in a different locale. Emulating RTT with Linux tc is a fallback—it approximates delay, not loss, jitter, or path MTU issues.
Example script (illustrative—not production-ready). Uses fictional hosts and thresholds. Adapt URLs, auth, and rates to your topology.
What this example demonstrates:
- Parallel regional scenarios:
journey_euandjourney_sarun at the same RPS split as analytics—not 100% from one POP. - Region tags: Every request carries
region:euorregion:safor percentile comparison in one summary export. - DNS realism:
BASE_EUandBASE_SAenv vars point at geo-steered hostnames your customers resolve—not a single global staging CNAME shortcut. - Journey shape: Multi-step flow (catalog + cart) exposes chatty APIs that RTT amplifies.
- Thresholds per region: Separate
p(99)gates when product SLOs differ by market.
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_EU = __ENV.API_EU || 'https://api-eu.staging.example.com';
const BASE_SA = __ENV.API_SA || 'https://api-sa.staging.example.com';
const TOKEN = __ENV.API_TOKEN || 'replace-me';
export const options = {
scenarios: {
journey_eu: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.EU_RPS || 20),
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 50,
maxVUs: 150,
exec: 'journeyEu',
},
journey_sa: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.SA_RPS || 12),
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 40,
maxVUs: 120,
exec: 'journeySa',
},
},
thresholds: {
'http_req_duration{region:eu}': ['p(95)<600', 'p(99)<1200'],
'http_req_duration{region:sa}': ['p(95)<900', 'p(99)<1800'],
http_req_failed: ['rate<0.02'],
},
};
function catalog(base, regionTag) {
const res = http.get(`${base}/v1/catalog?page=1`, {
headers: { Authorization: `Bearer ${TOKEN}` },
tags: { name: 'catalog', region: regionTag },
});
check(res, { 'catalog 200': (r) => r.status === 200 });
return res;
}
function addToCart(base, regionTag) {
const res = http.post(
`${base}/v1/cart/items`,
JSON.stringify({ sku: 'SKU-100', qty: 1 }),
{
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
},
tags: { name: 'add_to_cart', region: regionTag },
}
);
check(res, { 'cart 201': (r) => r.status === 201 });
}
export function journeyEu() {
catalog(BASE_EU, 'eu');
addToCart(BASE_EU, 'eu');
sleep(0.5);
}
export function journeySa() {
catalog(BASE_SA, 'sa');
addToCart(BASE_SA, 'sa');
sleep(0.8);
}
When you cannot deploy runners abroad yet, document that gap explicitly in the report—and pair with CDN cache headers load testing so at least edge behavior is not invisible.
Decision framework: distributed runners vs latency emulation
| Situation | Recommended action |
|---|---|
| Global product, SLOs per market | Runners in target regions; parallel scenarios with analytics-weighted RPS |
| Pre-prod budget, one staging stack | Emulate RTT cautiously + tag region:*; label results as approximate |
| CDN or geoDNS change this sprint | Resolver and hostname per region in env; compare before/after with same tags |
| Mobile backends with regional shards | Separate scenarios per shard URL; do not fold shards into one BASE |
| CI smoke only | Single region low RPS; full geo matrix nightly or weekly |
Use distributed runners when release gates depend on market-specific p99—not lab ping.
Use RTT emulation when you need directional signal before infra budget lands—never treat it as certification.
Use per-region hostnames when geoDNS is part of the product contract—internal staging.global shortcuts fake the journey.
Observability and pre-release checklist
Geo tests fail silently when tags and DNS setup drift. Before you scale traffic:
- Document RPS split per region and the analytics source (gateway logs, mesh, or CDN).
- Record which resolver and hostname each runner used—screenshot or env export in run notes.
- Set alerts when
p(99){region:sa}exceeds epsilon vs baseline whileregion:eustays flat. - Correlate k6
regiontags with APMgeooravailability_zonedimensions during incidents. - Archive scenario JSON, regional env vars, and git SHA per release (load testing in CI/CD for smoke; full matrix scheduled).
How Performate coordinates multi-region scenario packs
Duplicating entire repos per locale does not scale. Concrete workflow for the EU + South America journey above:
- Import one collection with catalog and cart requests duplicated into folders
EUandSA(different base URLs or host headers). Problem solved: one source of truth for journey steps. - Create two scenarios—
journey_euat 20 req/s,journey_saat 12 req/s—matching last week’s regional mix. Problem solved: honest weights without hand-editing executor blocks per tuning pass. - Apply tags:
region:euandregion:saon each scenario plus sharedroute:checkout_path. Problem solved: reports match the k6 tag model in this article. - Run both and open comparison view—filter by region and check whether SA p99 still meets the looser SLO while EU carries higher volume. Problem solved: one export for product and SRE.
- When marketing shifts traffic, change RPS in the editor and re-run—same requests, new mix. Problem solved: geo shifts are parameter changes, not repo forks.
- Export k6 for scheduled multi-region jobs on runners your infra team provides.
That maps to the cta: coordinate multi-region scenario packs with consistent tagging.
Closing takeaway
Geo distributed load testing is a routing and residency problem disguised as a latency chart. Run generators where users are—or be explicit when you emulate—and tag every request family by region so tail divergence blocks releases that look fine from one POP.
Run this week’s analytics-weighted regional mix before your next CDN or geoDNS change—and note which region still owns the p99 your SLO names.
Ready to optimize your API performance?
Use Performate to coordinate multi-region scenario packs with consistent tagging.