A lease can expire while its holder is paused, yet the old process can resume and send a delayed write. The storage service needs a way to reject superseded ownership. A fencing token supplies an ordered ownership epoch that travels with each mutation. Its protection depends on the resource checking the token, not merely on a lock service issuing one.
A lock alone is not enough
A client may check its lease, pause during garbage collection, and resume after another client acquires the resource. Checking immediately before sending does not eliminate that gap. Network delay can also deliver an already-sent request late. The resource must decide whether to accept the arriving operation even when the client’s own view of ownership is obsolete.
A lease timeout is a failure-detection mechanism, not proof that the previous process stopped. Credentials may still authenticate that process while its ownership has expired. Keep authentication, authorization, and the ownership epoch distinct. Fencing does not replace access control, and a malicious client must not be allowed to claim an arbitrary enormous token that permanently excludes legitimate owners.
Bind an increasing number to ownership
The coordinator issues an increasing token when ownership is acquired. A paused holder retains its older token; it cannot simply request a fresh number without acquiring ownership again. The sequence must survive failures without rollback or reuse. Wall-clock timestamps and independent counters on partitioned nodes do not automatically provide that contract.
The protected resource maintains the greatest accepted epoch. It rejects lower tokens and advances its high-water mark when a higher one arrives. That comparison and the associated data mutation must be atomic and durable together. Separate checking followed by an unguarded write recreates the race. A replicated resource needs a consistent enforcement boundary, not unrelated per-replica comparisons that can accept conflicting owners.
Equal tokens are a separate policy
A lease holder often performs several writes with the same epoch. Rejecting every token equal to the high-water mark would limit that holder to one accepted write. The usual epoch rule rejects lower values; equality remains eligible under the application’s ordinary permissions. Strictly increasing per-operation tokens are another protocol and should not be confused with one token per ownership acquisition.
Equality does not establish whether an operation is a duplicate. Use operation identifiers, idempotent updates, or another explicit ordering rule when retries must not repeat effects. Fencing separates ownership epochs; it does not order all operations within one epoch, deduplicate requests, or guarantee exactly-once business execution.
When it breaks
Issuing a new token does not instantly inform every protected resource. An older write arriving before that resource observes the new epoch can still pass the high-water check. If the contract requires immediate revocation at acquisition, establish the new epoch at the resource through an appropriate coordinated step before allowing protected work. Do not describe ordinary fencing as cancellation of every in-flight request.
Counter rollback can make a new owner receive a lower token than an old one. Losing the resource’s persisted high-water mark can also reopen an old epoch. Recovery must preserve both the issuing sequence and the resource state. Restoring one side from an older backup without reconciling ownership can invalidate the assumptions even when each component starts successfully.
Missing or malformed tokens must not silently bypass the guard. Audit every mutation path, including maintenance jobs and compatibility clients. A file service or external API that cannot enforce the token needs another supported concurrency mechanism; attaching a number to a request header alone provides no protection.
Check it quickly
Save this standard-library Python example as fencing.py and run it. It is a sequential in-memory model, not a durable lock service or a concurrent storage implementation:
class StaleFenceError(RuntimeError):
pass
class FencingResource:
"""Sequential model: equal tokens permit more writes in the same epoch."""
def __init__(self):
self.last_seen = 0
self.data = None
def write(self, token, value):
if type(token) is not int or token <= 0:
raise StaleFenceError("missing or invalid fence token")
if token < self.last_seen:
raise StaleFenceError(f"stale token {token} < {self.last_seen}")
self.last_seen = token
self.data = value
return True
class Issuer:
"""Test counter; a real issuer must bind durable epochs to ownership."""
def __init__(self):
self.counter = 0
def acquire(self):
self.counter += 1
return self.counter
def refused(resource, token):
before = (resource.last_seen, resource.data)
try:
resource.write(token, "must-not-land")
except StaleFenceError as error:
assert (resource.last_seen, resource.data) == before
print("rejected:", error)
else:
raise AssertionError("invalid or stale write was accepted")
issuer = Issuer()
old_leader, new_leader = issuer.acquire(), issuer.acquire()
resource = FencingResource()
# Issuance alone has not informed this resource about epoch 2.
assert resource.write(old_leader, "arrived-before-new-epoch")
assert resource.write(new_leader, "from-new-leader")
refused(resource, old_leader)
assert resource.write(new_leader, "second-write-in-same-epoch")
assert resource.last_seen == 2
refused(resource, None)
refused(resource, "2")
refused(resource, True)
next_token = issuer.acquire()
assert resource.write(next_token, "newest")
refused(resource, new_leader)
assert (resource.last_seen, resource.data) == (3, "newest")
print("PASS: observed newer epochs fence older writes; equal epoch remains usable")
The first old-token write succeeds because the resource has not observed epoch two. After epoch two writes, epoch one is rejected without changing either stored value. Another epoch-two write succeeds, demonstrating legitimate repeated work by one owner. Missing, string, and boolean tokens are rejected, and epoch three subsequently excludes epoch two.
The final PASS line confirms these comparisons. It does not validate persistence, atomic transactions, coordinator authorization, partition recovery, or network timing. Preserve those distinctions when turning this model into an integration test: the production resource must enforce the same ownership boundary at the point where the actual side effect happens.
Further reading
Sources
- Redis distributed locks, fencing recommendation and distributed-lock limitations, checked 2026-10-15.
- How to do distributed locking, increasing acquisition tokens and resource-side rejection of older epochs, checked 2026-10-15.