API Testing interview question
How do you test rate limiting, concurrency and race conditions in an API?
Short answer
For rate limits, send a controlled burst and assert 429 with Retry-After at the documented threshold, per key, then recovery after the window. For concurrency, fire truly parallel requests at shared state (stock, balances, unique fields) and assert invariants, optimistic locking (409/412) and idempotency keys.
Answer
These are bugs that single-threaded functional tests never find, and they tend to be expensive: oversold stock, double charges, or an API that falls over because one client loops.
Rate limiting
First get the policy: which algorithm (fixed window, sliding window, token bucket), what limit, and keyed on what (user, API key, IP, endpoint, tenant).
Tests:
- Send exactly the limit within the window: all succeed.
- Send limit + 1: the extra request gets 429 Too Many Requests, with a
Retry-Afterheader and any documented headers (X-RateLimit-Remaining, or the IETFRateLimit/RateLimit-Policyfields). - After the window or
Retry-After, requests succeed again. - Two different keys do not share a bucket; one noisy tenant cannot starve another.
- Behind a load balancer with several instances, the limit is enforced globally (shared counter in Redis or the gateway), not N times the limit.
- Expensive endpoints such as login, OTP and password reset have stricter limits; this is also a security control against brute force.
- The 429 response is cheap: it must not do the expensive work first.
Use a dedicated test key or environment with a small configured limit so tests are fast and do not disturb others.
Concurrency and race conditions
A sequential loop of requests is not a race test. Requests must hit the server at the same time. In Java, release threads together with a latch:
int n = 20;
ExecutorService pool = Executors.newFixedThreadPool(n);
CountDownLatch start = new CountDownLatch(1);
List<Future<Integer>> results = new ArrayList<>();
for (int i = 0; i < n; i++) {
results.add(pool.submit(() -> {
start.await();
return given().spec(authSpec)
.body(Map.of("sku", "LAST-ONE", "qty", 1))
.post("/orders")
.statusCode();
}));
}
start.countDown();
long created = 0;
for (Future<Integer> f : results) {
if (f.get() == 201) created++;
}
pool.shutdown();
assertEquals(1, created, "only one order may claim the last unit");
Run it several times; races are probabilistic. A load tool (k6, JMeter) can do the same at higher volume.
What to target
- Inventory / seats: stock of 1, many buyers; exactly one succeeds, stock never negative.
- Balances: parallel withdrawals must not exceed the balance (double spend).
- Uniqueness: parallel signups with the same email create one user, the rest get 409.
- Lost updates: two clients read version 3 and both PUT; with optimistic locking (
ETag+If-Match, or a version field) the second gets 412 or 409 instead of silently overwriting. - Idempotency keys: the same key sent concurrently produces one effect, and the second request either waits or gets a defined conflict response.
- State machines: cancel and ship the same order simultaneously; the end state must be valid.
Asserting
Assert invariants on the final state through the API or database, not only individual responses: count of orders, stock level, ledger sums. Check logs for deadlocks and 500s.
What interviewers look for
- Knowing what to assert for rate limits (429, headers, reset, scope), and that limits must hold across instances.
- Generating real parallelism rather than a sequential loop.
- Choosing high-risk shared-state scenarios and asserting invariants.
- Understanding the server-side fixes (optimistic locking, idempotency keys, unique constraints) well enough to test them.
A strong answer covers
- Rate limit tests: threshold, 429 status, Retry-After and limit headers, window reset
- Limits are scoped correctly (per user, API key, IP, endpoint) and distributed across instances
- Fires genuinely parallel requests (threads, latch, load tool)
- Asserts invariants: no oversell, no double spend, uniqueness
- Knows server-side protections: optimistic locking with ETag/If-Match, 409/412, idempotency keys