What a 429 means and how clients should react to one — then the mechanism that produces it: rate normalization, the derived burst capacity, and a play-by-play of the token bucket under a real spike.
There is no <MaxBurst> element in Apigee. Burst capacity is
derived from your rate — which is exactly why 30ps and 1800pm
are not the same policy, despite being the same throughput.
Retry-After — seconds, or an HTTP dateA 429 is a signal, not a failure. It tells you the server has a rate model and you're currently outside it. The rest of this deck is what that model looks like inside Apigee — and why the same "requests per second" can produce wildly different 429 behaviour.
Retry-After first — it beats any formula you inventRandomising the full range beats adding a small random nudge to a fixed delay — it decorrelates clients fastest and empirically minimises total work under contention.
Apigee never thinks in "per minute". It converts your rate into a drip interval — one token every interval.
both produce the identical drip →
Identical. Same interval, same 30 requests/second average. If the interval were the whole
story, 30ps and 1800pm would be interchangeable.
They are not — and the next slide is why.
"The bucket size is always configured to be
10% of the messagesPerPeriod."
— Apigee SpikeArrest policy reference
| Rate | messagesPerPeriod | Interval | Average | Bucket |
|---|---|---|---|---|
| 30ps | 30 per 1 s | 33.3 ms | 30/s | 3 |
| 1800pm | 1800 per 60 s | 33.3 ms | 30/s | 180 |
Capacity is derived from messagesPerPeriod — not from the rate you mean.
Express 30/s per second and that number is 30. Express it per minute and it's 1800.
Same throughput. Sixty times the burst tolerance.
One token every interval — for both our configs, one every 33.3 ms (30/second). The bucket fills whether or not anyone is calling.
The min() is the point: idle time buys burst capacity,
but only up to the cap. Idle an hour and 30ps still gives you 3.
Enough in the bucket? It passes. Not enough? 429 — immediately.
A rejected request consumes nothing and is never buffered or retried server-side. It is simply gone.
There is no queue, no waiting room, no smoothing buffer. Every request is decided the instant it arrives — pass or 429. Which is why the client's backoff is the only thing that recovers the traffic.
240 requests at once. The full bucket empties in 120 ms — everything after that is shed.
A polite client: one request every 33.3 ms — exactly the drip. Level holds flat. Steady state.
2 s lull, then 150 at once. The lull refilled only ~61 tokens. Burst tolerance regrows at the drip rate.
Both policies are "30 requests per second." Identical traffic in.
1800pm absorbs the entire spike without a single 429.
30ps passes 4 of 60 — its bucket of 3, plus the one token that dripped in during the burst.
Nothing in the config hints at this; the difference lives entirely in the units.
pm buys you 60× the burst| Throughput | as ps | bucket | as pm | bucket |
|---|---|---|---|---|
| 10/s | 10ps | 1 | 600pm | 60 |
| 30/s | 30ps | 3 | 1800pm | 180 |
| 100/s | 100ps | 10 | 6000pm | 600 |
Pick ps for hard smoothing — a fragile backend that must never see a clump.
Pick pm when real clients arrive in clumps — apps waking up, cron jobs, a page firing six calls at once.
With no <MaxBurst>, the units of <Rate> are your only burst control
— and they move it in 60× steps. At 30 req/s there is nothing between a bucket of 3 and a bucket of 180.
Each distinct client_id gets its own independent token bucket. One app's spike
stays that app's problem.
<SpikeArrest name="SA-Protect"> <Rate>1800pm</Rate> <Identifier ref="client_id"/> </SpikeArrest>
Capacity planning: your worst case is no longer Rate — it's
Rate × active clients. Size for one caller, then confirm the backend survives N of them bursting at once.
pm vs ps is your only burst control<Identifier ref="client_id"/>Retry-After on the 429 so clients can back off correctly30ps and 1800pm as interchangeable — 60× apart<MaxBurst>. It does not exist.Sizing shortcut. To pass a spike of B straight through, set
Rate = (B × 10)pm — which simultaneously pins your average at B/6 per second.
The two are not independently selectable.
And the other half of the job is on the client. A correctly sized bucket still produces 429s during a spike — exponential backoff with full jitter is what turns those into successful retries instead of a second spike.