Verdict
Yes
Yes, give each group of rate-limited routes one integration test that lowers the limit in the test configuration, sends one request over it and expects a 429; do not test the counting algorithm of the rate-limiting library.
Why
- Blast radiususers
- Change frequencyrarely
- Detectabilityeventually
- Reversibilitywith-effort
- Test costmoderate
Yes, test rate limits minimally: one test per group of limited routes that sends one request over the limit and expects a 429. The typical case is a web API whose middleware limits each client by IP address or API key. Blast radius is users, because a limit that stops counting lets one client slow the service for everyone, and Change frequency is rarely, because the limiter and the proxy in front of it change a few times a year. Detectability is eventually and Reversibility is with-effort, because a dead limit looks like normal traffic until a scraper finds it, and the fix is a config change plus cleanup of bot signups. Test cost is moderate, because the test runs the web layer with a lowered limit, so rule R12 gives Test minimally.
| When | Decision | Why |
|---|---|---|
| The limit guards login, password reset or one-time codes against guessing | Test mandatory: assert that attempts over the limit for one account are refused when they come from several IP addresses | Blast radius rises to safety-or-legal and Detectability to never, because a guessed password looks like a normal login |
| The limit caps text messages that you pay for per message, such as phone verification codes | Test mandatory: assert that a request over the limit per phone number sends no message | Blast radius rises to money and Reversibility to impossible, because the provider bills every message it sends |
| New routes arrive most months, and each one must join a rate-limited group | Test: one test reads the route table and fails for any route that has no limiter and is not on an allowlist | Change frequency rises to regularly, because every new route can miss the limiter |
| A managed API gateway enforces the limit, and only a deployed stage runs it | Test it differently: alert when a client's request rate stays above its limit with no 429 responses | Test cost rises to heavy while Detectability stays eventually and Reversibility with-effort |
| The only failure you expect is a limit set too low, which users report as 429 errors the same day | Do not test the limit value; watch the 429 rate per route after each change to the value | Detectability falls to same-day and Reversibility to trivial, because a refused request stores nothing |
What breaks if you don't test
A rate limit fails open with no error. A change made for another reason breaks the limiter: a proxy setting changed to fix addresses in the logs, a middleware moved below the router, or a second instance added. The express-rate-limit memory store does not share state across processes or servers, so three instances give each client three times the limit. Every request still gets a 200, and the first sign is a database alert during a scrape.
What you lose if you over-test
The common over-test sends the production limit, such as 1,000 requests per hour, and sleeps until the window resets. With an hourly window, that test adds up to an hour to CI and fails whenever a window boundary falls mid-run. Tests of a token bucket's refill timing check the library, which has its own tests.
How to test
Write integration tests with your framework's HTTP test client, and reset the limiter's store between tests. The minimum set:
- For each group of limited routes, set the limit to five in the test configuration, send six requests, and assert that only the sixth gets a 429, the status code from RFC 6585.
- Behind a proxy, give each request a different forged first entry in
X-Forwarded-Forand assert that they share one limit, because Express withtrust proxyset totruetakes the left-most entry as the client's address.
When the answer changes
- The limit protects login, password reset or one-time codes.
- A request over the limit costs you money, such as a text message.
- A managed gateway that tests cannot run enforces the limit.
Real incident + Code example
The header that turned off the limit
On an Express API for a property-listing site I worked on, the search routes allowed 300 requests per client IP address per 15 minutes. After we moved behind a load balancer, a developer set app.set('trust proxy', true) so that the logs showed real client addresses. The express-rate-limit docs warn that this setting reads the left-most X-Forwarded-For entry, which a client can set. Eleven days later a scraper sent a new made-up address with each request and copied all 60,000 listings in one night, until a database CPU alert fired. We changed the setting to 1, which trusts only the address the load balancer appends, and added this test, which fails with true:
import request from "supertest";
import { buildApp } from "../src/app.js";
test("forged X-Forwarded-For entries share one client's limit", async () => {
const app = buildApp({ searchLimit: 5 }); // production allows 300
const statuses = [];
for (let i = 0; i < 6; i++) {
const res = await request(app)
.get("/api/search?city=leeds")
// The client forges the first entry; the load balancer appends the real address.
.set("X-Forwarded-For", `10.9.8.${i}, 203.0.113.7`);
statuses.push(res.status);
}
expect(statuses).toEqual([200, 200, 200, 200, 200, 429]);
});
Related questions
FAQ
- Should I write an integration test that verifies rate limits?
Yes, write one integration test per group of rate-limited routes. Lower the limit to five in the test configuration, send six requests and expect a 429 on the sixth, so the test never waits for a time window.
- Should I test how my code handles a 429 from another API?
Yes, test your client's handling of another API's 429 when a dropped call loses work, such as a nightly sync. Point the client at a fake server that answers 429 with a
Retry-Afterheader, and assert that the client waits and retries.- Is rate limiting per IP address enough?
No, a per-IP rate limit does not stop credential stuffing, because attackers spread requests across many addresses, as OWASP's credential stuffing cheat sheet notes. Add a limit per account on login and one-time codes, and test it with requests from several addresses.