Summary

  • ONC RPC placed a 32-bit XID at the start of every call and copied it into the reply. The field let a client correlate messages and gave a server an equality key for a possible retransmission, but it was not a sequence number or proof of one execution.
  • NFS exposed the missing state. Volatile duplicate-request caches reduced replay damage but failed after eviction or crash; NFSv4.1 added bounded session slots, per-slot sequence IDs and cached replies so stronger execution claims had an explicit memory cost.

Silence created two plausible histories

A client sends a remote request to remove a file. The server may execute it, but the reply never reaches the client. After a timeout, the client sends the call again. From the client's side, two histories remain compatible with the same silence: the first call vanished before execution, or the operation completed and only its reply vanished.

The retry is therefore both necessary and dangerous. If the server treats it as new, it can repeat a side effect. If it treats every similar call as old, it can suppress a legitimate later request. No fact inside the timeout resolves the difference.

This was not hidden by the original remote-procedure metaphor. RFC 1050, published in April 1988, said that RPC did not try to provide reliability. RFC 1057, which replaced it two months later, retained the awkward rule: over UDP, no reply reveals nothing about the number of executions; a reply establishes at least one.

The uncertainty sits between message delivery and committed effect. A network can lose a call, lose a reply or break after the server changes state. The client observes the same timeout in all three cases.

XID answered the smaller question

Every version-2 RPC message starts with an unsigned 32-bit transaction identifier, or XID. A reply carries the XID of the call that initiated it. If several calls are outstanding, the client can attach each returning result to the right local waiter.

That is correlation, not chronology. RFC 5531 says the service side may compare XIDs for equality when detecting retransmissions, but cannot treat the field as a sequence number. The value contains no standard server epoch, execution counter, expiry rule or identity of the side effect.

The base protocol also made reuse conditional. A client application may reuse the previous XID when retransmitting; a server may remember that value and decline to execute a matching call again. Those two choices can produce some degree of execute-at-most-once behavior. Neither is supplied by the integer alone.

If the client assigns a new XID to its retry, equality cannot connect the copies. If the server forgot the old XID, equality finds nothing. If a value is eventually reused in a different context, equality without requester, procedure and lifetime can connect the wrong events. The identifier is useful because a local system gives it scope.

A matching reply did not certify the execution ledger

RPC kept authentication fields separate from XID. The reply number was never a signature, credential or proof that an authorized procedure committed exactly once. Even an authenticated message would answer who protected that exchange, not automatically how many times application state changed.

The transport distinction was also narrower than it sounded. RFCs 1831 and 5531 say that a reply over a reliable transport permits an exactly-once inference in their model. But they immediately preserve the hard case: when no reply arrives, the caller cannot assume that the procedure did not run, and timeouts plus reconnection remain necessary after server failure.

Reliability can order and deliver bytes on one connection. It cannot resurrect the application history that disappeared with a crashed process, nor can it decide whether a post-reconnect retry refers to work committed before the break. The boundary reappears whenever the recovery context changes.

NFS made the ambiguity operational

The early Network File System deliberately preferred stateless servers. RFC 1094 explained the attraction: after a crash or network interruption, a client could keep retrying instead of rebuilding protocol state with the server.

To make that workable, NFS made operations idempotent where possible. Repeating a read or a write to the same range could often have the same intended effect. Yet the specification marked several operations as possibly non-idempotent. A name can be removed only once. A rename that succeeded the first time can fail the second because its source name has already moved.

Idempotence reduced the cost of ignorance; it did not remove ignorance. RFC 1813 later gave a sharper NFSv3 example. Repeating a truncate-like non-idempotent request could destroy writes performed after the first copy. Even a connection-oriented transport could not eliminate duplicate processing after a break and automatic reconnection.

The phrase “same request” now needed an operating definition. It was not enough for two packets to resemble one another. The server needed to know which requester, which RPC procedure, which XID and which retained outcome belonged together.

The duplicate cache bought a window, not eternity

Many NFSv3 servers kept a duplicate request cache. After completing a call, the server remembered its completion status. If the same request returned while the entry remained present, the server sent the stored answer instead of applying the operation again.

This was a real correctness mechanism, not merely a performance cache. It moved the decision from the client's guess to evidence held at the execution boundary. But RFC 1813 was explicit about the limit. Typical cache state lived in RAM and vanished on a crash. A finite cache could evict an entry during a long partition before the client received the original reply. A later duplicate would then look new.

The XID had not failed at being an XID. The retained context had expired. Calling the number an idempotency key would conceal the controlling facts: who stored the first result, how the lookup was scoped, how long the entry survived and how failover was handled.

NFSv3 sometimes embedded stronger evidence in the operation itself. Exclusive CREATE used a verifier associated with the created object because volatile duplicate-cache memory was not sufficient for the required semantics. The extra evidence was local to that operation; it did not transform every RPC XID into durable history.

Bounded slots made stronger memory affordable

NFSv4.1 changed the shape of the problem. RFC 5661 introduced sessions, and the current specification, RFC 8881, describes a bounded slot table with a sequence ID and cached reply for each slot.

A requester chooses an unused slot. The first request on it carries a defined sequence value; each new use advances that value. A retry repeats the current sequence. The replier can then distinguish three cases locally: the next new request, a retransmission of the current one, or a misordered value. A completed retry receives the cached reply instead of another execution.

The slot matters as much as the sequence. It bounds the number of outstanding requests and therefore the number of replies the server may need to remember. A new sequence also tells the server that the requester has moved past the old result, allowing that slot's cache entry to be replaced.

RFC 8881 contrasts this with an unstructured XID space. XIDs are opaque to the replier and calls can complete out of order. Perfectly retaining every possible 32-bit result would require an impractical history. A negotiated slot table converts an unbounded naming problem into a finite custody obligation.

Exactly once still had a persistence boundary

NFSv4.1 did not repeal failure. Its exactly-once semantics depend on the reply cache and recovery state surviving the failures across which the claim is made. RFC 8881 notes that complete EOS across restart requires persistent cache state. A volatile implementation still improves ordinary retry handling, but it cannot testify about records it lost.

Nor did sessions abolish XID. The RPC layer still uses it to match replies and calls, including exchanges that do not follow the normal SEQUENCE path. The new mechanism did not promote a replacement identifier to universal authority. It added a narrower state machine where the application required a stronger outcome.

This is the historical progression: a number correlated messages; a cache gave equality short-term consequences; a bounded session made the cost and lifetime of stronger memory explicit. Each layer answered a different question.

What one number never authorized

An XID does not prove that two calls have identical arguments, that they came from the same authenticated principal or that the server remained in the same boot epoch. A cache hit does not prove durable commit. A cache miss does not prove a request is new. An apparently idempotent operation is not safe under every ordering; RFC 8881 shows how an older delayed write can overwrite a later one even when repeating the same write would normally have the same effect.

The correct claim is smaller. XID gives running endpoints a shared comparison token. Once-only execution emerges only when the executor keeps enough scoped state to connect the token with the work, the result and the recovery boundary.

Sources and evidence limits

  • RFC 1050 records the April 1988 RPC version-2 specification.
  • RFC 1057 records its June 1988 successor and the unreliable-transport execution limits.
  • RFC 1831 carries the RPC model into the later standards sequence.
  • RFC 5531 states the current Standards Track RPC version-2 contract.
  • RFC 1094 explains the early NFS stateless-server objective.
  • RFC 1813 documents NFSv3 retry hazards and the duplicate-request-cache boundary.
  • RFC 5661 introduced NFSv4.1 sessions and their slot machinery.
  • RFC 8881 contains the current NFSv4.1 description, including reply-cache and persistence conditions.

These documents define protocols and report design experience. They do not measure current cache sizes, persistence policies, deployment shares or implementation conformance.