skip to content

A client calls DELETE on a resource and gets HTTP 204; the identical DELETE is sent again and returns HTTP 404. Has the server broken DELETE's idempotency guarantee? Explain what that guarantee actually covers.

level: middleimportance: must knowfreq 62%

answer

  1. Idempotency = end state, not status code
  2. 204 then 404 is legal; so is 204 twice
  3. 404-on-repeat forces clients to treat 404 as success
  4. 410 Gone when you keep a tombstone
  5. Breaks for DELETE /queue/head and per-call counter decrements

basics

~20 s

No. Idempotency constrains the server's end state, not the response. After one DELETE or five, the resource is gone — that is the guarantee. Returning 204 then 404 is legitimate. Returning 204 both times is equally legitimate; pick one and document it.

solid answer

~50 s

Nothing is broken. RFC 9110 §9.2.2 defines idempotency as "the intended effect on the server of multiple identical requests is the same as for a single such request" — it says nothing about the status code or body. After the first DELETE the resource is absent, and after the second it is still absent, so the state converges. That is the whole promise. Both common designs are valid: return **404** on the repeat because that URI now has no resource, or return **204** unconditionally because the caller's desired end state ("gone") holds. The 204-always variant is friendlier to retrying clients, which otherwise have to treat 404 as success; the 404 variant is more honest about resource existence. Choose one and document it. Where DELETE genuinely stops being idempotent is when the URI does not identify a fixed resource — `DELETE /queue/head` removes a different message each call — or when deletion triggers a cumulative side effect such as decrementing a shared counter per call.

code

http · 16 lines
http
DELETE /orders/7 HTTP/1.1
Host: api.example.com

HTTP/1.1 204 No Content

DELETE /orders/7 HTTP/1.1
Host: api.example.com

HTTP/1.1 404 Not Found

--- or, the retry-friendly convention ---

DELETE /orders/7 HTTP/1.1
Host: api.example.com

HTTP/1.1 204 No Content

go deeper

for a junior

State clearly that idempotency is about the server ending in the same state, and that 204 then 404 does not violate it.

for a middle

Add the design trade-off between 404-on-repeat and unconditional 204, and note that clients' retry code depends on which you document.

for a senior

Push into where DELETE genuinely breaks — non-fixed URIs like /queue/head, per-call counter decrements, events emitted on every call — and describe making handlers converge rather than act.

for a principal

Frame it as observable-effect convergence across the whole system, not just the row: emitted events, downstream consumers, tombstone retention, and what convention you standardize across every service so client retry logic is uniform.

## What the guarantee says RFC 9110 §9.2.2: a method is idempotent if "the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request." Two words carry all the weight. **Effect on the server** — the guarantee is about state, not about what comes back on the wire. Any expectation that idempotent means "identical response every time" is imported from somewhere else, usually from functional purity. It is not in HTTP. **Intended** — the guarantee describes what the client is asking for, so a server that fails partway or a resource concurrently modified by someone else does not retroactively make DELETE non-idempotent. So the 204-then-404 sequence is fine. Before: the resource exists. After one DELETE: gone. After two: still gone. The states converge, which is the entire requirement. ## Why this trips people up The intuition "same request, same answer" comes from caching and from pure functions. HTTP deliberately separates the three properties: - **Safe** — the request does not ask for state change. - **Idempotent** — repeats converge on the same state. - **Cacheable** — the response may be reused for later requests. GET is all three. DELETE is only idempotent. Nothing in the idempotency definition constrains status codes, so a repeat DELETE returning 404 ("there is no such resource here now") is as legal as returning 204 ("your requested end state holds"). The same reasoning explains why an idempotent GET can return different bodies on successive calls: another client wrote data in between. Idempotency never promised a stable representation. ## Choosing 404 or 204 for the repeat This is a real design decision with real consequences. **Return 404 on the repeat.** Most literal reading of resource semantics: the URI no longer identifies anything, so 404 is the accurate status. Cost: every retrying client must now write `if (status == 204 || status == 404) treatAsSuccess`, and clients that forget will report false failures for a delete that actually succeeded. This is the single most common source of noisy alerts after a network blip. **Return 204 (or 200) unconditionally.** Reads DELETE as declarative — "ensure this is gone" — and reports success whenever that end state holds. Retry loops become trivially correct and the operation composes cleanly with at-least-once delivery. Cost: a caller with a typo'd ID gets a cheerful 204 and never learns the resource never existed, and you lose the ability to distinguish "deleted by me" from "never there". A middle path some APIs take: 204 when this call performed the deletion, 404 when the resource is unknown *and* you keep tombstones long enough to answer accurately; 200 with a body indicating `alreadyDeleted: true` when you want to be explicit. Whatever you pick, put it in the API documentation, because clients cannot infer it and their retry logic depends on it. Note that **410 Gone** is available when you deliberately remember that a resource used to exist and want to say so permanently — it is more informative than 404 for tombstoned entities, though it obliges you to keep the tombstone. ## When DELETE really is not idempotent Idempotency is a property of the method *as applied to a resource*, and a badly modelled URI can destroy it. - **Non-fixed target.** `DELETE /queue/head` or `DELETE /stack/top` removes a *different* item on each call. Repeating it drains the queue. The method contract is broken because the URI does not identify a stable resource. - **Cumulative side effects.** If deleting a comment also decrements `post.comment_count` on every call rather than recomputing it, the second DELETE corrupts the counter even though the comment stays gone. - **Deletion emitting events per call.** If each DELETE publishes a `CommentDeleted` event unconditionally, downstream consumers see duplicates. The stored state may converge while the observable system state does not. The fix in all three is the same: make the handler *converge*. Target a fixed URI, make counters derived or guarded, and emit the event only on the transition from present to absent. ## Interview-ready summary Idempotency is a promise about where the server ends up, not about what it says. 204 then 404 keeps that promise. The interesting engineering is (a) picking a repeat-response convention that makes client retries easy and documenting it, and (b) making sure your handler's side effects converge too — because a counter you decrement on every call is a genuine idempotency violation in a way that a 404 never was.

  • Which repeat-DELETE convention would you choose for an API whose clients retry aggressively, and why?
    Unconditional 204 (or 200) when the resource is absent, because it makes retry logic correct by default — the client never has to special-case 404 as success and a lost response after a successful delete costs nothing. The trade is that a caller with a wrong ID never learns it; if that matters, a 200 with an `alreadyDeleted` flag preserves both properties.
  • Give an example where DELETE is genuinely not idempotent.
    `DELETE /queue/head` removes a different message on every call, so retrying it drains the queue — the URI does not identify a fixed resource. Another case is a handler that decrements a shared counter on each call rather than recomputing it, which corrupts the counter on the second request even though the target stays deleted.
  • When would you return 410 Gone instead of 404 for a deleted resource?
    When you deliberately retain a tombstone and want to state that the resource existed and is permanently removed, rather than that nothing was ever there. 410 is more informative for caches and clients — it signals not to retry the URI — but it requires you to keep enough deletion history to answer accurately.

saying these in an interview costs you the question

  • Claiming the server broke idempotency because the two status codes differ.
  • Believing idempotency requires byte-identical responses.
  • Assuming 404 on a repeat DELETE means the first delete failed.
  • Not seeing that per-call side effects (counter decrements, emitted events) can break idempotency even when the resource stays deleted.
  • Insisting only one of 204 or 404 is spec-legal for the repeat.

context