---
title: "Load Testing Your API from Multiple Regions"
description: "Generator placement changes the network path, regional routing, and source IPs in an API load test. Learn how to build a multi-region test that measures the system you intended to test."
canonicalUrl: "https://zuplo.com/blog/2026/08/04/load-test-from-multiple-regions"
pageType: "blog"
date: "2026-08-04"
authors: "nate"
tags: "API Gateway, API Best Practices"
image: "https://zuplo.com/og?text=Load%20Testing%20Your%20API%20from%20Multiple%20Regions"
---
Where your load generators run changes nearly every number in the report, yet it
is one of the few test settings nobody seems to choose. The hosted platform
picks a default region, someone accepts it, and the entire baseline inherits
that network path.

A generator in the same cloud as your backend can make the direct path look
artificially fast. A few generators near one city can send all traffic to one
region of a multi-region deployment. Generator placement is test design, not
administrative detail.

## Run comparisons outside your backend's cloud

Cloud providers carry traffic between their own regions and services over
private networks. A generator in the same provider as your backend can measure a
path that most users never take.

That creates a particularly bad direct-versus-gateway comparison. The direct
request can stay on the provider's network while the gateway request crosses the
public internet twice. The reported "gateway overhead" then includes two
different network paths.

![A direct-vs-gateway comparison where only the gateway leg leaves the cloud provider for the public internet.](/blog-images/2026-08-04-load-test-from-multiple-regions/same-cloud-baseline.png)

Run both legs from the same neutral locations, outside the backend's cloud. Keep
payloads, authentication, connection settings, and ramp patterns identical. The
intermediary should be the only variable.

We have rerun direct-versus-gateway comparisons from neutral infrastructure and
watched the measured worst-case overhead fall by roughly half. The gateway did
not get faster between runs. The original baseline had been using the cloud
provider's private network while the gateway leg crossed the public internet.

Hosted load-testing regions often run inside a major cloud, even when the zone
name only identifies a city. Check the underlying provider before selecting a
zone. Grafana Cloud k6, for example, documents its public zones with provider
names in each `loadZone` identifier.

<CalloutDoc
  title="Performance Testing Your API Gateway"
  description="The fair-comparison checklist: identical requests, matched ramps, warm-up, and neutral generator locations."
  href="/docs/articles/performance-testing"
  icon="book"
/>

## Test global routing and regional capacity separately

A multi-region system raises two different questions:

1. Does the global routing layer distribute production-shaped traffic as
   expected?
2. Can each region handle the share of traffic it is provisioned for?

Do not try to answer both with one test.

For the first question, spread generators across the regions and DNS resolvers
your users rely on. DNS-based load balancers often choose a destination based on
the resolver's location. Anycast systems route a generator to a nearby point of
presence. A few generators therefore exercise only a small part of the global
topology.

For the second question, bypass the global load balancer and target each
region's direct endpoint. Assign each scenario the request rate that region is
expected to carry.

![One metro's generators sending every request to Region A while Region B sits idle.](/blog-images/2026-08-04-load-test-from-multiple-regions/dns-pileup.png)

This k6 configuration sends 1,000 RPS to each regional endpoint:

```javascript
import http from "k6/http";

const REGION_A = __ENV.REGION_A_URL || "https://region-a.api.example.com";
const REGION_B = __ENV.REGION_B_URL || "https://region-b.api.example.com";

export const options = {
  discardResponseBodies: true,
  scenarios: {
    region_a: {
      executor: "constant-arrival-rate",
      exec: "regionA",
      rate: 1000,
      timeUnit: "1s",
      duration: "10m",
      preAllocatedVUs: 200,
      maxVUs: 500,
    },
    region_b: {
      executor: "constant-arrival-rate",
      exec: "regionB",
      rate: 1000,
      timeUnit: "1s",
      duration: "10m",
      preAllocatedVUs: 200,
      maxVUs: 500,
    },
  },
  thresholds: {
    dropped_iterations: ["count==0"],
    http_req_failed: ["rate<0.01"],
  },
};

export function regionA() {
  http.get(`${REGION_A}/v1/resource`);
}

export function regionB() {
  http.get(`${REGION_B}/v1/resource`);
}
```

Arrival-rate executors keep the configured rate independent of response time.
Each scenario gets its own VU pool, so this example can consume up to 1,000 VUs.
The previous article explains
[why API tests should usually use an open workload model](/blog/how-not-to-load-test-an-api).

For a production-shaped test on Grafana Cloud k6, distribute a scenario across
load zones:

```javascript
export const options = {
  // Add the scenarios and thresholds from the previous example.
  cloud: {
    distribution: {
      us: { loadZone: "amazon:us:ashburn", percent: 50 },
      asia: { loadZone: "amazon:jp:tokyo", percent: 50 },
    },
  },
};
```

Choose percentages from real traffic, not an even split by habit. Also verify
that none of the selected zones recreates the same-cloud bias from the previous
section.

## Verify where the requests actually went

The script describes where you intended to send traffic. DNS, anycast routing,
connection reuse, and load-balancer affinity decide where it arrived. The first
graph we check is DNS query volume. A multi-thousand-RPS run with only a few
hundred lookups tells you that a small client pool resolved once and kept
hitting the same answer.

Check these server-side signals after the run:

- Request count and error rate by region and point of presence
- DNS queries by record, resolver, and location
- Connections and requests per backend instance
- Load-balancer responses that never reached the gateway or origin

A high request count with very few DNS lookups usually means a small generator
fleet resolved once and reused the result. That may be correct for a regional
capacity test. It is a warning sign for a test meant to validate global
distribution.

Do not expect counts from every layer to match exactly during failure. An
instance can return a response and terminate before flushing telemetry. A load
balancer can emit a 5xx before the request reaches the gateway. Treat count
disagreement as evidence about where requests disappeared, not as a reason to
pick whichever dashboard looks best.

## Give the test enough source IPs

Several components key behavior on client IP:

- Per-IP rate limiters
- WAF and bot-detection rules
- Source-IP load-balancer affinity
- NAT and connection-tracking tables

Two generator hosts at thousands of RPS look like two abusive clients, not a
real user population. They can also concentrate traffic on a small subset of
backend instances.

Use enough generator hosts and addresses to match the behavior you want to
simulate. Confirm the source IP count with your hosted provider. If a WAF or bot
system sits in the path, arrange allowlisting before the test.

## Monitor the generators as carefully as the API

An overloaded generator often fails quietly. It sends less traffic than
requested, which reduces queueing at the target and makes the API look better.
When throughput plateaus without a matching target-side limit, we treat the
generator as guilty until its health metrics clear it.

Track generator CPU, memory, network throughput, open connections, file
descriptors, and dropped iterations. The
[k6 guide to large tests](https://grafana.com/docs/k6/latest/testing-guides/running-large-tests/)
recommends keeping CPU below 80% and memory below 90%.

Connection limits deserve special attention. A Linux host typically has a finite
ephemeral port range, and closed connections can remain in `TIME_WAIT`. A shared
NAT or stateful firewall also has a finite connection-tracking table. AWS
documents a limit of
[55,000 simultaneous connections per unique destination for each NAT gateway IP](https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-basics.html).
A load-test fleet behind one NAT can share that limit.

Connection reuse changes the pressure dramatically. Apache Bench
[disables keep-alive by default](https://httpd.apache.org/docs/2.4/programs/ab.html),
while k6 reuses connections unless you enable
[`noConnectionReuse`](https://grafana.com/docs/k6/latest/using-k6/k6-options/reference/).
Do not compare two tools at the same RPS without checking their connection
behavior.

If throughput plateaus while the target has capacity, inspect the generator
fleet before changing the test threshold. Adding hosts is usually safer than
tuning a small number of machines to their operating-system limits.

## Multi-region load test checklist

Before the run:

- Place generators outside the backend's cloud provider.
- Use the same locations and request settings for both legs of an A/B test.
- Separate global-routing tests from per-region capacity tests.
- Choose regional percentages from production traffic.
- Confirm the number of source IPs and arrange WAF allowlisting.
- Put generator health and `dropped_iterations` on the test dashboard.

After the run, compare request counts, DNS lookups, and errors across every
region and intermediary. A multi-region label in the load-testing tool does not
prove the target received a multi-region workload.

The next article covers what changes when the path includes
[proxies, gateways, WAFs, and CDNs](/blog/load-testing-gateways-and-cdns).

<CalloutSignup
  badge="Test it yourself"
  title="Run a fair comparison against a Zuplo gateway"
  description="Deploy a gateway, run both legs from neutral regions, and compare the result with the same workload and network path."
  features={[
    "Deploy near your users",
    "Published overhead numbers for the baseline",
    "Support for coordinating large test windows",
  ]}
  signupButtonText="Deploy a gateway"
  signupUrl="https://portal.zuplo.com/signup?utm_source=zuplo-blog&utm_medium=web&utm_campaign=load-testing-series"
  secondaryAction={{
    text: "Performance testing guide",
    href: "/docs/articles/performance-testing",
  }}
/>