intermediate18 min

Idempotency and Rate-Limit Behavior

Why sending the same request twice should sometimes be perfectly safe, and how to test an API's rate limiting without a real load-testing tool.

What you'll learn

  • Classify HTTP methods as idempotent or non-idempotent and explain what that guarantees
  • Design a test that verifies (or disproves) a claimed idempotency guarantee
  • Explain what a well-designed 429 rate-limit response should include

Prerequisites

Explanation

A mobile app sends a "charge this card" request, but the network drops before the response arrives. Does the app retry? If it does, and the first request actually succeeded on the server before the connection dropped, a naive retry charges the customer twice. Idempotency is the property that makes safe retrying possible: an idempotent operation produces the same end result no matter how many times it's repeated with the same input.

By HTTP convention, GET, PUT, and DELETE are supposed to be idempotent: fetching a resource repeatedly doesn't change it; replacing a resource with the same data repeatedly leaves it in the same final state; deleting an already-deleted resource is still "deleted" (whether it returns 204 or 404 the second time is an implementation choice, but the result — the resource being gone — doesn't change). POST is conventionally NOT idempotent, because it typically means "create a new thing" — sending the same POST twice conventionally creates two things, unless the API deliberately adds its own extra safety mechanism on top (commonly an "idempotency key" the client generates once and sends with every retry, letting the server recognize and safely ignore a duplicate).

Testing idempotency directly is refreshingly literal: send the same request twice (or three times) and verify the end state — not just the response — genuinely matches what one successful call alone would produce. A DELETE that's supposed to be idempotent but throws an unhandled 500 on the second call has broken its own contract, even if the first call worked perfectly.

Rate limiting is a related but distinct reliability concern: an API that allows unlimited requests per second from one client is vulnerable to being overwhelmed, accidentally or deliberately. A well-designed rate limit responds with status 429 (Too Many Requests) once a client exceeds its allowance, and a genuinely useful 429 response tells the caller when they can retry — typically via a Retry-After header or a field in the body — rather than leaving the client to guess and hammer the API blindly. A tester doesn't need a real load-testing tool to verify this logic exists and behaves correctly; the counting and threshold logic itself can be tested directly, the same way any other piece of business logic can.

Example

A simulated idempotent DELETE and a non-idempotent POST, tested by calling each twice and comparing end states.

const resources = { 1: "widget" };

function deleteResource(id) {
  const existed = id in resources;
  delete resources[id];
  return { status: existed ? 204 : 404 };
}

function createResource(name) {
  const id = Object.keys(resources).length + 1;
  resources[id] = name;
  return { status: 201, id };
}

console.log(deleteResource(1)); // 204 -- deleted
console.log(deleteResource(1)); // 404 this time, but the RESULT (gone) is unchanged -- idempotent
console.log(Object.keys(resources).length); // still reflects one delete, not two

Try it yourself

Call createResource twice with the same name and observe that (unlike delete) it creates two separate entries.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Classify each HTTP method's conventional idempotency using the strings 'idempotent' or 'not-idempotent': methodGet, methodPost, methodDelete.

Checks: GET correctly classified · POST correctly classified · DELETE correctly classified

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write a function checkRateLimit(requestCountInWindow, limit) that returns { status: 429, retryAfterSeconds: 60 } if requestCountInWindow >= limit, or { status: 200 } otherwise.

Checks: under the limit succeeds · exactly at the limit returns 429 with retry guidance · over the limit returns 429

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Assuming POST is always safe to retry without checking whether the API actually implements idempotency-key support.
  • Testing idempotency by checking only the response of a repeated call, without verifying the underlying end state actually stayed consistent.
  • Treating a 429 response with no retry guidance as acceptable, leaving clients to guess and potentially hammer the API blindly.

Knowledge check

Knowledge check

1. What does it mean for an HTTP operation to be idempotent?
2. Why is POST conventionally not idempotent?
3. What should a well-designed 429 Too Many Requests response include?

Takeaway

Idempotency is what makes safe retrying possible, and it's directly testable by calling an operation twice and checking the end state; rate limiting is testable business logic, not something that requires a real load test.

Summary

This lesson covered classifying HTTP methods by conventional idempotency, testing idempotency by verifying end state across repeated calls, and what a well-designed rate-limit response should provide.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.