Skip to main content

Avoid API Rate Limiting: 10 Common Causes

Avoid API Rate Limiting: 10 Common Causes

Key Takeaways

  • API rate limiting means the server answers 429 Too Many Requests once 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

#CauseTypical symptom in a testFirst fix
1Request volume above the quota429s from a fixed second onwardPace the test to the documented rate
2Redundant or repeated callsMany identical requests in the logCache and batch
3Ignoring Retry-After and quota headers429s keep coming right after the first oneRead the headers and pause
4Aggressive polling and unpaced paginationSteady high request count with little new dataWebhooks, larger pages, pacing
5Retries without backoff or jitter429 count grows after each limit hitExponential backoff with jitter
6All virtual users share one identityLimit hit far below expected user countModel realistic identities, or ask the provider
7Burst and token bucket rules misreadTest passes for a minute, then failsTest a sustained rate, not only a spike
8Plan or quota tier lower than productionLimit lower than what production getsTest on the tier you run
9CDN, proxy or WAF limits mistaken for the API’s403 or 429 with no quota headersCompare headers and logs from each layer
10No monitoring of 429s during the runFailures found after the testAlert 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.

ObservationLikely sourceWhat to check
429 with Retry-After and quota headers in the API’s formatThe API’s own limiterProvider documentation
403 or 429 with an HTML body or an unfamiliar server headerCDN or WAF ruleEdge or firewall logs, rule for automated traffic
Timeouts or 502/504 at peak, no 429Proxy timeout or overload, not rate limitingProxy logs, backend latency
Errors from one region or IP range onlyGeo or IP ruleRerun 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

  1. Read the provider’s rate limit page and write down the algorithm, the rate, the burst, the concurrency cap and the identity it counts.
  2. 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.
  3. 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.
  4. Handle 429 in the script: record it as its own metric, honour Retry-After, and back off.
  5. 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

Published with PostNext

Chris

Head of Content at LoadFocus

Chris leads content and oversees development across the services this blog covers. The guides and tool comparisons here come out of the same decisions that shape what ships.

Weekly tips right in your inbox.

Don’t miss out on the latest testing tips and tricks, tools and news related to software testing.

Already +20,000 members in the list.

The Load Testing Platform with AI That Explains Your Results

Load test websites and APIs from 25 cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.

Cloud load testing, website speed and API monitoring in one place.

Hi there! Can I share a cool thing I'm working on with you?

A software testing newsletter that sparks joy.

Our goal is to create helpful content for software developers and testers.

If you're not into it, you can unsubscribe instantly.