Key Takeaways
- API rate limiting means the server answers
429 Too Many Requestsonce a client passes a quota. In a load test that usually points at your test, not at a weak backend. - The ten causes below fall into three groups: too much traffic (volume, polling, redundant calls), clients that ignore the server’s signals (no
Retry-After, no backoff), and limits you did not model (one shared identity, burst rules, plan quotas, a CDN or WAF in front). - Read the provider’s rate limit page before the first test, count 429s separately from real errors, and never use extra keys to dodge a limit the provider set on purpose.
What does API rate limiting look like in a load test?
The server returns HTTP status 429. RFC 6585 defines it as “Too Many Requests” and says the response MAY include a Retry-After header telling the client how long to wait. The same RFC states that it does not define how the server identifies the user or counts requests, so every provider decides that for itself: per API key, per IP address, per endpoint, per account, or all of those at once.
Retry-After is defined in RFC 9110 and holds either a number of seconds or an HTTP date. Many APIs add their own headers for the quota. GitHub sends x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-used, x-ratelimit-reset and x-ratelimit-resource. Stripe sends a Stripe-Rate-Limited-Reason header on a 429 with values such as global-rate, endpoint-rate and global-concurrency. The X-RateLimit-* names are a convention, not a standard, so the meaning of a value can differ between providers.
There is also an IETF effort to standardise this. The HTTP API working group’s draft draft-ietf-httpapi-ratelimit-headers (version 11, dated 23 May 2026) defines RateLimit-Policy and RateLimit header fields. It is still an Internet-Draft that expires on 24 November 2026, not an RFC, so do not assume any given API sends these fields yet.
When a load test meets a limit, the symptom is a block of 429s, often starting at one fixed point in the run and clearing in steps. The latency of the successful requests can look fine, which hides the problem if you only watch averages. Count status codes, not just response times.
The 10 causes at a glance
| # | Cause | Typical symptom in a test | First fix |
|---|---|---|---|
| 1 | Request volume above the quota | 429s from a fixed second onward | Pace the test to the documented rate |
| 2 | Redundant or repeated calls | Many identical requests in the log | Cache and batch |
| 3 | Ignoring Retry-After and quota headers | 429s keep coming right after the first one | Read the headers and pause |
| 4 | Aggressive polling and unpaced pagination | Steady high request count with little new data | Webhooks, larger pages, pacing |
| 5 | Retries without backoff or jitter | 429 count grows after each limit hit | Exponential backoff with jitter |
| 6 | All virtual users share one identity | Limit hit far below expected user count | Model realistic identities, or ask the provider |
| 7 | Burst and token bucket rules misread | Test passes for a minute, then fails | Test a sustained rate, not only a spike |
| 8 | Plan or quota tier lower than production | Limit lower than what production gets | Test on the tier you run |
| 9 | CDN, proxy or WAF limits mistaken for the API’s | 403 or 429 with no quota headers | Compare headers and logs from each layer |
| 10 | No monitoring of 429s during the run | Failures found after the test | Alert on 429 rate while it runs |
The 10 causes in detail
1. Sending more requests than the quota allows
Every provider publishes or enforces a ceiling. GitHub’s REST API documents 5,000 requests per hour for an authenticated user’s personal access token, and a separate limit of 900 points per minute plus a cap of 100 concurrent requests. Stripe documents 100 requests per second in live mode and 25 per second in a sandbox, plus a concurrency limit of 50 in-progress requests in live mode (both read from the vendors’ docs on 2026-10-23). Numbers like these change, so treat them as examples of the shape, not as values to hard-code.
Two kinds of limit matter for testing. A rate limit counts requests over time. A concurrency limit counts requests in flight at once, so slow responses can trip it even at a modest request rate. A load test with many virtual users can hit either one.
2. Redundant or repeated API calls
Rate limits are often reached by application logic, not by real user growth. A dashboard where five widgets each poll the same status endpoint every 5 seconds makes 5 x 12 = 60 requests per minute. One batched call every 30 seconds, cached and shared by the widgets, makes 2 per minute. The data shown is the same, the request count is 30 times lower. This is an illustrative calculation, not a measurement.
In a load test the same thing happens when every virtual user fetches the same reference data (a product list, a config document) on every iteration. Fetch it once in a setup step, or cache it, and keep the per-iteration calls for what a real user would repeat.
3. Ignoring 429, Retry-After and quota headers
A client that treats 429 as an ordinary failure and sends the next request immediately measures how fast it can irritate the limiter. If the response carries Retry-After, wait that long. If it carries a quota header such as GitHub’s x-ratelimit-reset (a UTC epoch time), wait until then. GitHub’s documentation says to wait for the reset when x-ratelimit-remaining is 0, and warns that continuing to send requests while rate limited may get an integration banned.
Parse the headers per provider. Do not assume X-RateLimit-Remaining means the same thing everywhere, and treat the values as hints, since GitHub notes they can vary between responses because requests are handled in several regions.
4. Aggressive polling and unpaced pagination
Polling every two seconds from a handful of clients adds up quickly. Replace polling with webhooks or server-sent events when the API offers them. For paginated lists, request the largest page size the API allows, follow the cursor, and pause between pages when the provider asks for it. A loop that fetches page after page as fast as the network allows is a burst of its own.
5. Retrying without exponential backoff and jitter
Fixed-interval retries turn one 429 into a stream of them. The usual fix is exponential backoff: wait 1 second, then 2, then 4, up to a cap, and stop after a set number of attempts. Add jitter, which means randomising each wait, so that many clients do not all retry at the same instant. Amazon’s Builders’ Library article on timeouts, retries and backoff explains why: if all failed calls back off to the same time, they cause contention or overload again when they are retried.
The same article warns that retries multiply load. In its example of five layers each retrying three times, the database sees 243 times the load. Retry in one place in the call chain, and give retries a budget.
Stripe’s documentation recommends the same pattern for 429s: exponential backoff with randomness, and for heavy users a client-side token bucket that throttles traffic globally.
6. Every virtual user sharing one identity
If all virtual users send the same API key or come from the same IP address, the provider sees one client. A test with 500 virtual users can hit a per-key limit of a few requests per second immediately, and it says nothing about how 500 separate customers would fare. Cloud load generators also send from a limited set of IP addresses, so a per-IP limit can trigger earlier than you expect.
The temptation is to spread the load over many keys. Check the provider’s terms first: splitting traffic across keys to avoid a limit can breach them, and the result may still not match production. Better options are a dedicated test account or environment, a test allowance agreed with the provider in advance, or a mock of the third-party API. Stripe goes further and discourages load testing its sandbox, because sandbox limits are lower and its latency profile differs from live mode. It recommends a configurable mock that sleeps for a realistic time instead.
7. Misreading burst and token bucket limits
Many limiters use a token bucket. Tokens are added at a steady rate up to a maximum, and each request takes one. A client can spend a full bucket in a burst, then falls to the refill rate. Amazon API Gateway documents this directly: it throttles with the token bucket algorithm, with a steady-state rate and a burst, returns 429 Too Many Requests when both are exceeded, and describes the limits as best-effort targets, not guaranteed ceilings.
Here is a made-up example. A bucket holds 100 tokens and refills at 10 per second. A test that sends 100 requests in the first second sees no throttling. The same test sending 100 requests every second then passes about 10 per second after the bucket empties, and the rest get 429. Over the first 60 seconds the bucket allows 100 + 600 = 700 requests, not 6,000. A short spike test can therefore pass while a ten-minute sustained test fails, and both results are correct for what they measured.
Leaky bucket limiters smooth the outflow to a fixed rate instead. Fixed-window limiters reset a counter at the start of each interval, which lets a client send a full quota at the end of one window and again at the start of the next. Read the provider’s documentation to find which algorithm applies.
8. A plan or quota tier lower than production
Free and trial plans usually carry lower limits than paid ones. Testing on a trial key and reading 429s as a capacity problem gives a false alarm. Testing on a generous tier and shipping on a lower one hides a real one. Run the test on the plan production will use, and if the production quota is not enough for your expected peak, ask the provider for a higher limit before the event, not during it. Stripe, for example, asks for contact at least 6 weeks ahead for a large increase.
9. A CDN, proxy or WAF limit mistaken for the API’s
A web application firewall, CDN or API gateway can throttle or block a burst of automated traffic and return a 429 or a 403 even when the application behind it has no limit at all. The tell is in the response: a real API limit usually brings the provider’s quota headers or Retry-After and a JSON error body in the API’s format, while an edge rule often brings a different server header, an HTML error page or no quota headers.
| Observation | Likely source | What to check |
|---|---|---|
429 with Retry-After and quota headers in the API’s format | The API’s own limiter | Provider documentation |
403 or 429 with an HTML body or an unfamiliar server header | CDN or WAF rule | Edge or firewall logs, rule for automated traffic |
| Timeouts or 502/504 at peak, no 429 | Proxy timeout or overload, not rate limiting | Proxy logs, backend latency |
| Errors from one region or IP range only | Geo or IP rule | Rerun from another location |
If your own CDN or WAF is in front of the system under test, allow the load generator’s addresses for the test window. LoadFocus’s load testing runs from 25 cloud regions (loadfocus.com/load-testing, 2026-10-23), which also lets you check whether a block follows the region.
10. No monitoring of 429s during the run
A limit found after a test ends has already wasted the run. Track the share of 429 responses separately from 5xx and timeouts, per endpoint and per identity, and stop or pause the test when it passes a level you set. In a LoadFocus load test you can set a maximum error rate and P95 or P99 response time as pass/fail thresholds (loadfocus.com/load-testing, 2026-10-23), and a run of mostly 429s will fail the error rate threshold instead of passing quietly.
Outside tests, API monitoring can assert on the status code of each check and alert by email, Slack, Microsoft Teams, Discord, PagerDuty, Opsgenie or webhook (loadfocus.com/api-monitoring, 2026-10-23), so an unexpected 429 on a production check reaches someone. Our post on monitoring APIs covers the setup.
How to load test an API that has a rate limit
- Read the provider’s rate limit page and write down the algorithm, the rate, the burst, the concurrency cap and the identity it counts.
- Decide what you are testing. If it is your own backend, test it directly or behind a gateway you control, and keep the third-party limit out of the picture with a mock. If it is the integration with a third party, test at or below their documented rate and ask for permission first.
- Pace the load. In k6, an arrival-rate executor holds a fixed request rate regardless of response time, which makes it easy to stay under a published limit.
- Handle 429 in the script: record it as its own metric, honour
Retry-After, and back off. - Run a sustained test as well as a spike, because a token bucket forgives the spike.
A minimal k6 sketch of steps 3 and 4 (a sketch for illustration; check it against your k6 version and API before relying on it):
import http from 'k6/http';
import { sleep } from 'k6';
import { Counter } from 'k6/metrics';
const rateLimited = new Counter('rate_limited');
export const options = {
scenarios: {
paced: {
executor: 'constant-arrival-rate',
rate: 8, // requests per timeUnit, below the documented limit
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 20,
},
},
thresholds: {
rate_limited: ['count<1'],
http_req_duration: ['p(95)<800'],
},
};
export default function () {
const res = http.get('https://api.example.com/items');
if (res.status === 429) {
rateLimited.add(1);
const wait = Number(res.headers['Retry-After']) || 1;
sleep(wait);
}
}
The threshold on rate_limited makes the run fail the moment the pacing is wrong. For the full k6 setup see LoadFocus k6 load testing, and for REST APIs in general the guide to load testing RESTful APIs with LoadFocus. Our post on optimizing API performance testing covers the wider test design, and common API performance issues explains how to tell a rate limit from a slow backend.
Frequently Asked Questions
How do I know whether a 429 comes from the API or from a firewall?
Compare the response against the provider’s documentation. A real API limit normally comes with the provider’s quota headers or a Retry-After header and an error body in the API’s own format. A 403 or 429 with an HTML page, an unfamiliar server header and no quota headers points to a CDN or WAF in front. Rerunning from another location and reading the edge logs settles it.
What is the difference between a rate limit and a concurrency limit?
A rate limit counts requests over a time window, such as 100 per second. A concurrency limit counts requests being processed at the same moment, such as 50 in progress. Stripe documents both. Slow responses raise concurrency even when the request rate is flat, so a load test can hit a concurrency limit without exceeding the documented rate.
Is the RateLimit header an official standard?
Not yet. The IETF HTTP API working group’s draft draft-ietf-httpapi-ratelimit-headers (version 11, 23 May 2026) defines RateLimit-Policy and RateLimit fields, but it is an active Internet-Draft, not an RFC. Retry-After is standard (RFC 9110), and 429 is standard (RFC 6585). The X-RateLimit-* headers are a widely copied convention.
Should I add exponential backoff to a load test script?
Add it where it matches real client behaviour. Real clients retry, so a backoff with jitter tells you whether your system recovers after a limit. Keep it out of the measurement where you only want the raw capacity of the endpoint, and record the 429s either way.
Can I use several API keys to get past a rate limit in testing?
Only if the provider’s terms allow it. Many providers set limits per account or per key on purpose, and splitting traffic to avoid them can breach the terms. Ask for a test allowance or a test environment, or mock the third-party API in the test.
Does a token bucket allow bursts?
Yes. A token bucket lets a client use saved-up tokens in a short burst, then limits it to the refill rate. That is why a one-minute spike test can pass where a ten-minute sustained test fails.
How should I handle rate limits in a CI performance test?
Pace the load to stay below the documented rate, fail the build on a 429 count or error rate threshold, and keep the third-party API mocked unless you have an agreement with the provider. The continuous performance testing post covers pipeline setup.
Sources
- RFC 6585, section 4: 429 Too Many Requests (read 2026-10-23)
- MDN: Retry-After header, based on RFC 9110 (read 2026-10-23)
- IETF draft-ietf-httpapi-ratelimit-headers (version 11, read 2026-10-23)
- GitHub REST API rate limits (read 2026-10-23)
- Stripe rate limits (read 2026-10-23)
- Amazon API Gateway request throttling (read 2026-10-23)
- Amazon Builders’ Library: Timeouts, retries, and backoff with jitter (read 2026-10-23)
Published with PostNext