skip to content

After a remote procedure call fails without a reply, which procedures may the client safely re-invoke, and how do you decide?

level: middleimportance: must knowfreq 32%

answer

  1. twice equals once
  2. absolute state versus relative change
  3. the contract says, not the name
  4. where did it fail

basics

~20 s

A call is safe to re-invoke when two runs leave the same intended state as one: reads and absolute writes like 'set address to X'. 'Add 10 points', 'create order' or 'send email' are unsafe unless the server filters repeats.

solid answer

~50 s

Re-invoking is safe when the procedure is **idempotent**: its intended effect after two executions equals its effect after one, though the reply may differ. Reads with no side effects qualify; so do writes stated as an absolute end state (`setShippingAddress(order, addr)`) and conditional writes that apply only if a version still matches. Relative or generative procedures do not: `addLoyaltyPoints(10)`, `createOrder`, `sendReceiptEmail` each do more per run. Decide from the procedure's documented semantics, not its name or the transport — an RPC system cannot infer idempotence for you, since every procedure is a verb it knows nothing about. Two things widen the safe set: a failure that provably happened before the server's application logic saw the request makes any call safe to resend, and a server that filters repeats by request identifier makes a non-idempotent call safe to resend with the same identifier.

go deeper

for a junior

Recall the test: running it twice must leave the same state as running it once. Reads and absolute sets pass; adds, creates and sends fail.

for a middle

Explain idempotence precisely — intended effect, not identical reply — and the ways to make an unsafe procedure safe: absolute values, conditions, identifiers, split creation.

for a senior

Show judgment on real procedures: hidden per-call effects, late repeats clobbering newer writes, and using the failure's location to decide when even a create can be resent.

for a principal

Make retry-safety part of every interface contract and code review, so client teams never guess from method names and each procedure states whether it may be repeated.

## The test After an RPC fails without a reply, the client does not know whether the procedure ran (the request may have been lost, or only the reply). Resending is therefore safe only if running the procedure **again** would do no harm when the first attempt **did** run. That property is **idempotence**: the *intended effect* on the server of two identical executions is the same as one. Two clarifications keep the definition honest: - **The reply may differ.** A second `deleteUser(42)` may answer "not found" where the first answered "deleted"; the state is the same, so it is still idempotent. - **Incidental effects do not count.** Writing an access log line per call does not make a procedure non-idempotent; HTTP's RFC 9110 draws the same line for its methods, noting that a server "is free to log each request separately". ## Classifying procedures | Procedure | Shape | Safe to re-invoke? | Why | |---|---|---|---| | `getBalance(account)` | read | yes | no state change | | `setShippingAddress(order, addr)` | absolute set | yes | end state is the same after one or two runs | | `cancelOrder(order)` | absolute state change | usually | a cancelled order stays cancelled, unless cancelling also triggers a per-call refund | | `addLoyaltyPoints(customer, 10)` | relative change | no | each run adds 10 more | | `createOrder(cart)` | generative | no | each run creates another order | | `sendReceiptEmail(order)` | external effect | no | each run sends another email | | `updatePrice(item, 9.99, ifVersion=7)` | conditional set | yes | a repeat finds version 8 and changes nothing | The decision belongs in the **interface contract**: document for each procedure whether it is idempotent. Unlike HTTP, where the method itself (`GET`, `PUT`, `DELETE`) carries a published idempotence property, an RPC procedure is an arbitrary verb. The framework sees only a name and bytes; gRPC's own retry design, for example, states it provides no way to mark a method idempotent and leaves the choice to the service owner. ## Making an unsafe procedure safe 1. **Restate it as an absolute end state.** `setLoyaltyPoints(customer, 340)` instead of `add(10)`, where the caller knows the target. 2. **Make it conditional.** Apply only if the record's version is still the one the caller read; a repeat then changes nothing. 3. **Let the server recognise the repeat.** The client sends a request identifier and reuses it on resend; the server remembers it and replays the first reply. How such records are stored is a deduplication subject of its own. 4. **Split creation in two.** The client first obtains or chooses the new object's identifier, then calls `createOrder(orderId, cart)`; a repeat with the same `orderId` finds the order already there. ## When where it failed makes any call safe Idempotence answers "is a repeat harmless?". A second question can make it moot: **could the first attempt have run at all?** - If the client could not open a connection or never wrote the request, the procedure cannot have run, so even `createOrder` can be resent. - If the server's RPC layer refused the request before handing it to application code, the same holds. gRPC's retry proposal, for instance, resends such calls automatically for exactly this reason — they "have not made it to the server application logic, and thus are always safe to retry". - Anything later — the request was delivered and may have been dispatched — puts the call back into the unknown outcome, and only idempotence or duplicate filtering makes a resend safe. RFC 9110 states the general rule for HTTP clients: do not automatically retry a non-idempotent request unless there is some means to know it is actually idempotent, or "some means to detect that the original request was never applied". The same reasoning applies to any RPC. ## Two traps - **Late repeats of an idempotent call.** `setStatus(order, SHIPPED)` is harmless repeated in isolation, but a resend that arrives after another caller set `RETURNED` overwrites newer state. A conditional form avoids this. - **"Idempotent" procedures with hidden per-call effects.** A `cancelOrder` that issues a refund every time it runs is not idempotent, whatever its name suggests.

  • Is an idempotent setStatus(order, SHIPPED) always safe to resend late?
    Not always. Repeating it is harmless in isolation, but a resend that arrives after another caller moved the order to RETURNED overwrites the newer state. Idempotence is about repeating one call, not about ordering against other writers; a conditional form that applies only if the version is still the one the caller read keeps late repeats harmless.
  • Is a 'delete user' procedure safe to re-invoke after a lost reply?
    Only if it is written to be. If deleting an absent user is a no-op, a repeat is harmless even though it may answer 'not found'. If each run has its own side effect — a refund, a notification, releasing a reused name — a second run is not harmless, so the contract must say which kind it is.
  • How can a client know a failed call never reached the server's application logic?
    Only from where the failure happened: the connection could not be opened, the request was never written, or the server's RPC layer explicitly refused it before dispatch. Some frameworks resend those cases automatically for that reason. Anything after the request may have reached the handler is an unknown outcome again.

saying these in an interview costs you the question

  • Any call is safe to resend if the transport is reliable.
  • Idempotent means the procedure returns the same reply every time.
  • A get or set prefix in the method name proves it is idempotent.
  • The RPC framework can work out by itself which procedures are idempotent.
  • Logging every call makes a procedure non-idempotent.