1Example run (not a real result)
2run_id
3What this run changed.
Azure API Management versus client-side failover
Rich Leyshon
November 2, 2026

For engineers who already retry and fail over between AI model regions in client code, and wonder if a gateway is worth it. No Azure experience needed.
A gateway did not beat a well-built client. Nothing in the tests showed API Management (APIM) performing better. Success rates matched. APIM p95 answer time was worse, by about a quarter of a second in a clean test.
What mattered was a sin bin. When a region refuses a call,a circuit breaker makes it sit out for the next minute. Setups with a sin bin answered every request when one region ran short.
When every region is out of quota, nothing helps. Only more quota, or waiting for theresponse’s Retry-After header was better for success rate.
Regions differ in speed. From a UK laptop, UK South answered in about 300 ms, Sweden Central in 365 and West Europe in 480, and West Europe was the least steady.
What to build:
Should you stand up APIM? Do it for operations, not speed: one set of credentials, one place for rules and monitoring. Those benefits were not tested. Goodput did not make the case for APIM.
AI services ration tokens. Each model deployment has a quota, counted per minute. When it runs out, the service refuses the call with HTTP status 429 (“Too Many Requests”).
The usual fix is to deploy the same model in several regions and fall back between them. Most teams write that logic into the client. The alternative is a gateway: a server between your callers and the AI models that applies rules to every call. Azure API Management (APIM) is Microsoft’s gateway service.
A gateway is one more service to pay for, run and debug. Is it worth it? This article tests that.
Azure AI Foundry
Callers send requests to the gateway. The gateway picks a region. If that region is out of quota, it refuses with a 429. The gateway can then try another region, or take the refusing region out of rotation. A client-side setup does the same job without the middle box. The gateway holds the model credentials, so callers only need the gateway’s key.
| Term | Meaning |
|---|---|
| Quota | What a model deployment can handle each minute. Exceed it and calls are refused. There are two limits: tokens per minute (TPM, where a token is a chunk of text) and requests per minute. |
429 and Retry-After |
A 429 means “out of quota, try later”. The reply often carries a Retry-After header: how many seconds to wait. A 5xx error means the service itself failed. |
| Pool, weights, priority | The pool is the three regions behind one name. Weights set each region’s share: 90/5/5 sends 90% to UK South. Priority ranks the regions: a lower-ranked region gets traffic only when the higher ones are out. |
| Retry | Try a failed call again. The client can do it, or the gateway can. |
| Circuit breaker | The gateway’s sin bin. After a set number of failures, a region leaves the pool for 60 seconds. You choose which errors count. All callers share it. |
| Cool-down | The client’s sin bin: a timer that skips a region after it refuses a call. |
Three Azure AI Foundry regions each got a small quota, so quota exhaustion could be reached easily: UK South, West Europe and Sweden Central. The K6 load tool sent requests through the gateway, or straight to the regions. One setting changed at a time.
There were two stages of experiments:
Three situations were tested:
| Situation | What it models |
|---|---|
| Every region short | Demand beyond all your quota |
| Main region short | One region overloaded, the others have room |
| Nothing wrong | What the gateway costs when all is healthy |
A rule was fixed before the main test. The gateway counts as better if it gets more than 2 points more success, or has a p95 answer time more than 50 ms faster. In these experiments, the rule was never met.
Each deployment allowed 60 requests a minute (rpm). The test calls were tiny, so rpm limited rate before the token limit did. A gateway can move requests between regions but cannot raise that ceiling.
| Measure | Meaning |
|---|---|
| Success rate | Share of requests that got a valid answer |
| Time to answer | The median and p95 latencies in milliseconds |
| Throughput | Requests handled per second |
| Goodput | Answered requests per second (throughput × success rate) |
A gateway that refuses everything instantly has high throughput and zero goodput. Goodput is a better measure for meaningful work rate. The animation below visualises a goodput of 9 requests a second.
Each dot below is one run. Pick a situation to see its runs and what they showed.
Bursts. Short, sharp tests. 200 requests were fired as fast as possible at three small quotas (10,000 tokens a minute each), so every region ran out within seconds. The question was whether any setting helps when demand beats all the quota at once. Gateway setups, direct calls and client retries all took part.
Steady load. The same situation with a realistic load. Requests arrived at a steady 6 a second for five minutes against bigger quotas (60,000 tokens a minute each). In practice each region answered about 72 requests a minute, so together the three could serve 3.6 of the 6 requests a second. That caps any setup at 60% answered. Each gateway setup was paired with a client built to do the same job. The question was how close to that cap each one could get.
Nothing helped. In 200-request bursts, nearly every setup answered about 17%. A circuit breaker on an even split made it worse (4.5%). Only a client that waited as long as Retry-After asked got nearly everything through (99.5%). That took about 30 seconds per request.
Under steady load every setup answered 55-60%, at or close to the 60% cap, so there was little left for any setup to win.
One region overloaded, the others with room to spare. UK South was sent 90% of the traffic, more than its quota allowed, and the other two regions 5% each. Requests arrived at a steady 2 a second (filled dots) or 3 a second (hollow dots), when the fallbacks were busy too, for five minutes. Each gateway setup was paired with a client built to do the same job. The question was which setups steer calls to the regions with room. Dots at 100% are nudged sideways so they do not hide each other.
Here the setup mattered. UK South got 90% of the traffic, more than it could serve. The other two regions sat nearly idle.
| Setup | Success |
|---|---|
| Nothing | 70% |
| Retry only, in the gateway | 77% |
| Retry only, in the client | 79% |
| Sin bin in the gateway (circuit breaker or priority list) | 100% |
| Sin bin in the client (cool-down) | 100% |
| Client tries UK South first, nothing remembered | 100% |
Raising the load did not change this.
What a gateway costs when nothing is wrong. Requests arrived at a steady 1 a second for five minutes, split evenly across three regions with quota to spare, so nothing was ever refused. There was nothing to recover from, so any difference between the gateway and direct calls is the gateway’s own overhead. Hover over a dot for its answer times.
Both sides answered everything. The median answer took 372 ms through the gateway and 396 ms direct. The p95 was 710 ms against 593 ms. The gateway’s cost is in the slow tail. The next tab looks at that more closely.
A separate set of tests that measured speed alone. Quota was plentiful, each request got one attempt, and there was no retry, sin bin or cool-down. Three kinds of run were compared, shown as shapes. Through the gateway (circles) pinned the gateway to one region at a time. Direct, all regions together (diamonds) sent calls straight to the three regions in one run, alternating between them. Direct, one region at a time (squares) did the same one region per run, so the regions were never tested together. The question was how fast each region is from a UK laptop, and what the gateway adds.
| Region | Direct: median | Direct: p95 | Via gateway: median | Via gateway: p95 |
|---|---|---|---|---|
| UK South | 297 ms | 434 ms | 299 ms | 798 ms |
| Sweden Central | 365 ms | 675 ms | 396 ms | 929 ms |
| West Europe | 481 ms | 982 ms | 473 ms | 1,233 ms |
All of this was measured from one laptop in the UK on one day. A caller elsewhere could see a different order.
Retry-After for batch work only. Waiting out the minute suits a nightly job. It does not suit a chat.If success rate matters more than speed, start with client rules. Take a batch job such as transcription, where a few hundred milliseconds do not matter. The gateway’s one measured cost, a slower p95, stops mattering. But it never won on success either: a client with a sin bin matched the gateway’s success rate in every matched test. A gateway cannot create quota or fix retries on its own.
Client rules
Retry-After when every region was short. Easy to change.A gateway (APIM)
For a use-case where success matters more than speed
Retry-After and retry. It is slow, but latency is not the goal.Checks for anyone running this in production.
| What goes wrong | What to do |
|---|---|
| APIM retry silently does nothing on requests with a body. The retry fails inside the gateway and never reaches the model. | Add buffer-request-body="true" to the forward-request inside the retry. Then confirm the model sees more than one call per refused request. |
| A retry block with no forward-request returns instant fake 200s. | Alert on answers that come back far faster than the model can respond (here, 14 ms against hundreds). Check the body holds a real answer. |
count means retries, not attempts. count="3" is four attempts. |
Match your client’s attempts to the gateway’s. |
| Policy changes take up to two minutes. Breaker state survives a redeploy. | Wait two minutes after a change before you trust it. |
| The request limit can bite before the token limit. Here it was 60 requests a minute alongside 60,000 tokens. | Read your limits first: az cognitiveservices account deployment show, then properties.rateLimits. |
| Turning a breaker off with an empty rules list is rejected. | Set the breaker to null in Terraform or ARM. |
Every run, as a card. Each tab holds runs that share a quota and load, so compare cards within a tab. Cards run from highest goodput to lowest, with any reference card first (the speed tab runs fastest to slowest). The first four tabs are the gateway-against-client tests: a coloured edge is the gateway and a grey edge is a client calling the regions directly. The fifth is the region speed tests, which measured answer time only. The last five are the 200-request bursts. A reference card is pinned on most tabs. Hover a numbered note to highlight that part of the card.
| Gateway | APIM Developer tier, UK South |
| Regions | Azure AI Foundry in UK South, West Europe and Sweden Central |
| Model | gpt-4.1-mini, answers of about 5 tokens, one fixed prompt |
| Quota | 10,000 TPM per region for the bursts, 60,000 TPM for everything else |
| Load tool | Grafana k6 on a Mac laptop |
| Bursts | 200 requests from 20 simulated callers, sent as fast as possible |
| Steady runs | 1, 2, 3 or 6 requests a second for five minutes |
| Gaps | 60 seconds between runs, 2 minutes after a policy change |
| Success | HTTP 200 with a valid answer in the body |
| Discarded | Steady runs longer than 330 seconds (the laptop slept) or with dropped requests |
| Speed tests | Rebuilt regions and gateway, 60,000 TPM per region, one request every 2 seconds per region for about 11 minutes, first 20 per region ignored |
The Terraform, k6 scripts and results are in r-leyshon/azure-load-balance-perf.
How to read a card
2run_id
3What this run changed.
5Completed 160 of 200 requests in 16.7 s · 10,000 TPM per region
Steady 2 requests a second for five minutes at 60,000 TPM per region, with 90% of traffic aimed at UK South, which can only serve about two thirds of that. Cards with a coloured edge go through the gateway; grey-edged cards are a client calling the regions directly. Where a setup was repeated, the card shows the middle result of its valid runs.
car_90_5_5, car_90_5_5_r2, car_90_5_5_r3
No breaker and no retry. UK South is offered about 108 requests a minute against a ceiling near 72, and the other two regions are nearly idle.
3 runs: 70.4%, 69.8%, 69.6%
Median answer 326 ms · p95 856 ms
p6a_apim_priority (r1, r2, r5)
UK South is first choice and the others are used only when it is out of the pool. Breaker trips after one failure.
3 runs: 100.0%, 100.0%, 100.0%
Median answer 365 ms · p95 882 ms
Azure Monitor, one isolated run: 1.01 model calls per request, 4 of them refused
2 more runs discarded (laptop slept)
p5a_direct_weighted_mem (r1, r2, r3, r4, r5), p6a_direct_weighted_mem (r1, r3, r4, r5)
As above, but after a refusal that caller skips that region for 60 seconds.
9 runs: 100.0%, 100.0%, 100.0%, 100.0%, 100.0%, 100.0%, 100.0%, 100.0%, 100.0%
Median answer 367 ms · p95 638 ms
Azure Monitor, one isolated run: 1.08 model calls per request, 45 of them refused
1 more run discarded (laptop slept)
p6a_apim_retry_cb_trip1 (r1, r2, r3, r4, r5)
The same, but UK South leaves the pool after its first refusal.
5 runs: 100.0%, 100.0%, 100.0%, 100.0%, 100.0%
Median answer 379 ms · p95 888 ms
Azure Monitor, one isolated run: 1.01 model calls per request, 4 of them refused
p5a_direct_seq_mem (r1, r2, r3, r4, r5), p6a_direct_seq_mem (r1, r4, r5)
The client tries UK South, then the next region, and skips a region for 60 seconds after a refusal.
8 runs: 100.0%, 100.0%, 100.0%, 100.0%, 100.0%, 100.0%, 100.0%, 100.0%
Median answer 358 ms · p95 708 ms
1.10 model calls per request
2 more runs discarded (laptop slept)
p5a_direct_seq (r4, r5)
The client tries UK South, then the next region, with nothing remembered between requests.
2 runs: 100.0%, 100.0%
Median answer 362 ms · p95 770 ms
1.40 model calls per request
p6a_apim_retry_cb (r1, r3, r4, r5)
UK South leaves the pool for 60 seconds after three refusals, so retries can only reach the other two regions.
4 runs: 100.0%, 100.0%, 100.0%, 100.0%
Median answer 385 ms · p95 1,044 ms
Azure Monitor, one isolated run: 1.02 model calls per request, 12 of them refused
1 more run discarded (laptop slept)
p5a_direct_weighted (r1, r2, r3, r4, r5), p6a_direct_weighted (r1, r3)
No gateway. The client picks a region by weight and retries up to twice more, picking again each time.
7 runs: 81.9%, 80.5%, 79.7%, 79.0%, 78.5%, 78.4%, 77.9%
Median answer 428 ms · p95 714 ms
1.93 model calls per request
1 more run discarded (laptop slept)
p6a_apim_retry_no_cb_int0 (r2, r3)
The same, with no pause between attempts.
2 runs: 79.2%, 75.0%
Median answer 382 ms · p95 995 ms
1 more run discarded (laptop slept)
p6a_apim_retry_no_cb (r1, r2, r3, r4, r5)
The gateway retries a refused call up to twice more, waiting one second each time. Nothing is remembered, so each retry picks a region by weight again.
5 runs: 79.5%, 79.2%, 76.7%, 76.5%, 76.4%
Median answer 402 ms · p95 2,635 ms
Azure Monitor, one isolated run: 1.94 model calls per request, 687 of them refused
car_cb5xx_90_5_5
The breaker ignores 429s, so running out of quota never removes UK South. No retry.
One run: 69.9%
Median answer 312 ms · p95 539 ms
Steady 3 requests a second for five minutes at 60,000 TPM per region, with 90% aimed at UK South. The two fallback regions must take about 108 requests a minute between them, three quarters of what they can serve. Grey-edged cards are a client calling the regions directly.
p6b_apim_retry_cb (r1, r2, r3)
UK South leaves the pool for 60 seconds after three refusals.
3 runs: 100.0%, 100.0%, 100.0%
Median answer 368 ms · p95 1,104 ms
p6b_apim_retry_cb_trip1 (r1, r2, r3)
UK South leaves the pool after its first refusal.
3 runs: 100.0%, 100.0%, 100.0%
Median answer 354 ms · p95 872 ms
p5b_direct_seq_mem (r1, r2, r3)
As above, but a region that refused is skipped for 60 seconds.
3 runs: 100.0%, 100.0%, 99.9%
Median answer 350 ms · p95 706 ms
1.14 model calls per request
p5b_direct_weighted_mem (r1, r2, r3)
The client picks a region by weight, retries, and skips a region for 60 seconds after a refusal.
3 runs: 100.0%, 100.0%, 100.0%
Median answer 340 ms · p95 563 ms
1.09 model calls per request
p6b_apim_priority (r1, r3)
UK South first, the others only when it is out of the pool. Breaker trips after one failure.
2 runs: 100.0%, 100.0%
Median answer 350 ms · p95 842 ms
1 more run discarded (laptop slept)
p5b_direct_seq (r1, r2, r3)
No gateway. Every request tries UK South first and moves on after a refusal.
3 runs: 100.0%, 100.0%, 100.0%
Median answer 478 ms · p95 830 ms
1.83 model calls per request
Steady 6 requests a second for five minutes at 60,000 TPM per region, about 1.7 times what the three regions can serve (3.6 a second). The best any setup can do is about 60%. Grey-edged cards are a client calling the regions directly.
p5d_direct_weighted_mem_eq (r1, r2, r3)
The client picks a region at random, retries, and skips a region for 60 seconds after a refusal.
3 runs: 60.0%, 59.9%, 59.8%
Median answer 452 ms · p95 786 ms
2.21 model calls per request
p5d_direct_seq_mem (r1, r2, r3)
No gateway. The client tries UK South, then the next region, and skips a region for 60 seconds after a refusal.
3 runs: 57.7%, 57.5%, 57.2%
Median answer 365 ms · p95 706 ms
0.75 model calls per request
p6d_apim_priority (r1, r2, r3)
UK South first, the others only when it is out of the pool. Breaker trips after one failure.
3 runs: 57.1%, 56.7%, 55.9%
Median answer 359 ms · p95 922 ms
p6d_apim_retry_cb_trip1_eq (r2, r3)
Equal weights, retry, and a breaker that trips after one failure.
2 runs: 57.1%, 54.9%
Median answer 350 ms · p95 858 ms
1 more run discarded (laptop slept)
Steady 1 request a second for five minutes at 60,000 TPM per region, an even split and no failures. This measures what the gateway costs when nothing is wrong. The grey-edged card is the reference.
p5c_direct_equal (r1, r2, r3)
No gateway and no failures: the client splits calls evenly across the three regions.
3 runs: 100.0%, 100.0%, 100.0%
Median answer 396 ms · p95 593 ms
1.00 model calls per request
p5c_apim_baseline (r1, r2, r3)
The same traffic through the gateway with equal weights and no breaker. Nothing is refused, so no recovery is needed.
3 runs: 100.0%, 100.0%, 100.0%
Median answer 372 ms · p95 710 ms
Speed only, with no quota pressure: one request every 2 seconds per region for about 11 minutes, no retry, breaker or cool-down, and the first 20 answers per region ignored. A longer bar means a faster answer. A coloured edge is the gateway, pinned to one region; a grey edge is a direct call. Every valid run had no refusals or errors. Two runs that dropped a request were repeated, and Azure Monitor confirmed that every gateway call reached its pinned region.
rlB_uksouth (r1)
No gateway. Each run called one region only, so the regions were never measured together.
One run · about 300 answers each
rlA (r1), rlA_r2_x2, rlA_r3_x2
No gateway. One run called all three regions in turn, so the regions were measured side by side, at 0.5 requests a second each.
3 runs · about 300 answers each
Across runs: median 296-306 ms · p95 428-442 ms
rlC_uksouth (r1, r2, r3)
Through the gateway, pinned to this one region. No retry and no breaker.
3 runs · about 300 answers each
Across runs: median 297-310 ms · p95 729-874 ms
rlB_swedencentral (r1)
No gateway. Each run called one region only, so the regions were never measured together.
One run · about 300 answers each
rlA (r1), rlA_r2_x2, rlA_r3_x2
No gateway. One run called all three regions in turn, so the regions were measured side by side, at 0.5 requests a second each.
3 runs · about 300 answers each
Across runs: median 361-372 ms · p95 656-749 ms
rlC_swedencentral (r1, r2, r3)
Through the gateway, pinned to this one region. No retry and no breaker.
3 runs · about 300 answers each
Across runs: median 383-402 ms · p95 806-973 ms
rlC_westeurope (r1, r2, r3)
Through the gateway, pinned to this one region. No retry and no breaker.
3 runs · about 300 answers each
Across runs: median 459-517 ms · p95 1,062-1,286 ms
rlA (r1), rlA_r2_x2, rlA_r3_x2
No gateway. One run called all three regions in turn, so the regions were measured side by side, at 0.5 requests a second each.
3 runs · about 300 answers each
Across runs: median 450-764 ms · p95 816-1,212 ms
rlB_westeurope (r1)
No gateway. Each run called one region only, so the regions were never measured together.
One run · about 300 answers each
200-request bursts, 10,000 TPM per region. With no retry, nothing gets past about 17%. A breaker or a request cap on its own makes it worse, and the busiest run completes nothing.
baseline
Equal weights across the three regions. No circuit breaker, no retry.
Completed 35 of 200 requests in 5.1 s · 10,000 TPM per region
asymmetric_cb
90% of traffic lands on UK South, which runs out first and can trip the breaker. No retry.
Completed 33 of 200 requests in 3.9 s · 10,000 TPM per region
cb_5xx_only
The breaker ignores 429s and only trips on 5xx, so running out of quota never removes a region. No retry.
Completed 33 of 200 requests in 5.6 s · 10,000 TPM per region
weighted_primary
UK South takes 60% of the traffic, West Europe 25%, Sweden Central 15%. Still no breaker and no retry.
Completed 33 of 200 requests in 5.9 s · 10,000 TPM per region
circuit_breaker
Equal weights. A region leaves the pool after three failures inside 60 seconds, and a 429 counts as a failure. Nothing retries onto whoever is left.
Completed 9 of 200 requests in 2.4 s · 10,000 TPM per region
gateway_throttle
The gateway refuses calls beyond 10 a minute, before Foundry sees them.
Completed 7 of 200 requests in 2.4 s · 10,000 TPM per region
full_resilience
Weights, a circuit breaker and a 500 requests-a-minute gateway cap, with no retry. Every call was refused in about 12 ms, too fast to have reached the model.
Completed 0 of 300 requests in 1.3 s · 10,000 TPM per region
200-request bursts, 10,000 TPM per region. Calling Foundry directly gives the same result as the gateway with no policies. Walking the regions in order only burns calls.
baseline
Equal weights across the three regions. No circuit breaker, no retry.
Completed 35 of 200 requests in 5.1 s · 10,000 TPM per region
direct_weighted
No gateway. The client picks a region at random (60/25/15) and does not fall back when that region is exhausted.
Completed 33 of 200 requests in 3.0 s · 10,000 TPM per region
direct_sequential
No gateway. The client tries UK South, then West Europe, then Sweden Central, moving on only after a failure. It made 567 HTTP calls to land 33 successes.
Completed 33 of 200 requests in 5.3 s · 10,000 TPM per region
direct_equal
No gateway. The client splits calls evenly across the three regions and does not fall back. The matched pair for the control.
Completed 35 of 200 requests in 11.4 s · 10,000 TPM per region
200-request bursts, 10,000 TPM per region. Short pauses do nothing useful. Only sleeping for the full Retry-After works, and it costs about 30 seconds a call.
baseline
Equal weights across the three regions. No circuit breaker, no retry.
Completed 35 of 200 requests in 5.1 s · 10,000 TPM per region
retry_immediate
The client retries a 429 straight away, up to three times, against an equal-weight pool with no breaker.
Completed 35 of 200 requests in 7.0 s · 10,000 TPM per region
retry_backoff
Client retry with exponential backoff: 500 ms, then 1 s, then 2 s.
Completed 26 of 200 requests in 40.2 s · 10,000 TPM per region
retry_backoff_jitter
Backoff plus jitter, so the virtual users do not all wake up on the same tick.
Completed 26 of 200 requests in 41.8 s · 10,000 TPM per region
retry_fixed_1s
The same client retry, with a fixed one second pause between attempts.
Completed 24 of 200 requests in 40.5 s · 10,000 TPM per region
optimal_asym_client
Skewed 90/5/5 weights and a breaker on 429 and 5xx, with the client sleeping for Retry-After.
Completed 72 of 200 requests in 124.1 s · 10,000 TPM per region
optimal_equal_client
A repeat of the same setup, run again under another name.
Completed 198 of 200 requests in 426.2 s · 10,000 TPM per region
retry_after_header
The client sleeps for the Retry-After value Azure returns, usually the rest of the quota window.
Completed 199 of 200 requests in 456.4 s · 10,000 TPM per region
200-request bursts, 10,000 TPM per region. A gateway retry rule was set in these runs but never worked (a bug found later), so read them as breaker and weights only. Nearly all land at the same 16-18%, and two identical runs of one setup differ widely.
baseline
Equal weights across the three regions. No circuit breaker, no retry.
Completed 35 of 200 requests in 5.1 s · 10,000 TPM per region
trip_count_1
90/5/5 weights, with the breaker tripping on the first failure instead of the third.
Completed 33 of 200 requests in 3.3 s · 10,000 TPM per region
weight_70_15_15
A gentler skew than 90/5/5, with the breaker on 429 and 5xx.
Completed 33 of 200 requests in 4.1 s · 10,000 TPM per region
weight_80_10_10
An in-between skew: the spare regions get 10% each, with the breaker on 429 and 5xx.
Completed 33 of 200 requests in 4.3 s · 10,000 TPM per region
apim_retry_asym
Skewed weights and a breaker on 429 and 5xx. UK South is expected to trip first.
Completed 36 of 200 requests in 9.7 s · 10,000 TPM per region
asym_retry_no_cb
Skewed weights with the breaker off.
Completed 36 of 200 requests in 12.1 s · 10,000 TPM per region
apim_retry_equal
Equal weights and no breaker. A gateway retry rule was set but never worked, so this is the control again.
Completed 37 of 200 requests in 12.8 s · 10,000 TPM per region
optimal_equal_apim
A repeat of the same configuration, run again under another name.
Completed 36 of 200 requests in 12.7 s · 10,000 TPM per region
asym_retry_cb_5xx
Skewed weights, breaker on 5xx only. Running out of quota never trips the breaker.
Completed 34 of 200 requests in 12.5 s · 10,000 TPM per region
optimal_asym_apim
A repeat of the identical configuration, run again under another name. The two results differ.
Completed 11 of 200 requests in 6.8 s · 10,000 TPM per region
apim_retry_cb
Equal weights and a breaker on 429 and 5xx: the same as the breaker-only run, run again. The gateway retry rule set here never worked.
Completed 9 of 200 requests in 8.1 s · 10,000 TPM per region
200-request bursts, 60,000 TPM per region: six times the quota, so 200 requests fit inside one minute's allowance. A gateway retry rule was set but never worked, so these are even-split and skewed controls. The control from the 10,000 TPM runs is pinned for contrast.
baseline
Equal weights across the three regions. No circuit breaker, no retry.
Completed 35 of 200 requests in 5.1 s · 10,000 TPM per region
apim_retry_equal_r2
The even-split control with six times the quota. 200 requests fit inside one minute's quota. A gateway retry rule was set but never worked.
Completed 193 of 200 requests in 5.6 s · 60,000 TPM per region
asym_retry_no_cb_r2
Skewed weights with the breaker off, at 60,000 TPM.
Completed 190 of 200 requests in 7.4 s · 60,000 TPM per region
optimal_equal_apim_r2
The same configuration again, run on its own later.
Completed 181 of 200 requests in 14.2 s · 60,000 TPM per region
Microsoft documents these pieces in backends, set-backend-service, retry, rate-limit-by-key, AI gateway capabilities, how policies work and APIM key concepts. The setup and the result files are in github.com/r-leyshon/azure-load-balance-perf.
Thanks to Harry, Francis and Adela for their thoughts and feedback.
fin!