Payments are experiencing issues due to temporary restrictions in Russia. If your payment does not go through, please submit a support request.Our support team is available 24/7 — we are always here to help with hosting and server issues.We are now accepting requests for dedicated server rental and colocation services in our data center.Reminder: we recommend enabling backups for additional data protection.A new VPS/VDS lineup with NVMe storage and improved performance is now available.Maintenance work on some servers has been completed. All services are operating normally.
Article8 min readViews0

TTL Lock Expired: How a Fencing Token Stops an Old Worker

On a safe model, we show why lease expiration does not stop an old process and how a monotonic fencing token prevents it from overwriting the new owner's result.

Comments 0

Two autonomous robots near an industrial valve: a new gear token accepted, the old one stopped by the mechanism
In this article

If a background worker receives a task lease for five seconds, that does not mean it physically stops executing code after the fifth second. The process may hang, lose contact with the coordinator, or simply pause for a long time, only to resume after another worker has taken over the task. For any write operation that affects data correctness, a fencing token is required—a monotonically increasing owner identifier. The storage accepts the new number and rejects any older one.

In the small example below, the first worker receives token 1, survives the lease expiration, and attempts to write the old product remainder. The second worker has already received token 2 and written a newer value. A standard TTL lock allows late overwrites, while token verification preserves the new owner's result.

What is documented and what is verified here

The documentation for Hazelcast FencedLock 5.7 describes a scenario with client pause, session loss, and a new owner; the external resource must reject requests with an old monotonic token. etcd Repository contains an executable example with the same boundary: the lease may expire, so the protected storage checks the request version.

The training run does not use Hazelcast, etcd, Redis, a network, or 1C-Bitrix. It separately reproduces an invariant on CPython 3.12.14: after token 2 is accepted, the record with token 1 must fail. This is a model of the mechanism, not a report on implementation at a client site.

Initial conditions

  • CPython 3.12.14 and the standard library only; the run was performed on November 2, 2026.

  • Isolated model without network, files, database, or background processes; all data is defined in code.

  • Time is set in whole conditional seconds. Worker A receives a lease from t=0 to t=5, Worker B attempts to enter at t=4 and is rejected, then receives a new lease at t=6.

  • Each successful lease issuance increments the token: A receives 1, B receives 2.

  • First, a storage without token verification is used, then a storage that atomically compares the number with the last accepted one.

Moment

Event

Expected result

t=0

A receives the lease and token 1

A can start working

t=4

B requests a leased resource

LockBusy rejection

t=5

A's lease expires

Code A does not stop on its own

t=6

B receives token 2 and writes the remaining product quantity 8

New state accepted

t=7

A resumes and attempts to write 5

Write with token 1 rejected

Two workers receive tokens 1 and 2: the late record from the first worker is rejected after the second worker's record
Training timeline: lease A ends at t=5, B receives token 2 at t=6, and the late record A with token 1 is rejected at t=7.

Reproducible model

Create a new temporary directory, save the following code to the file fencing_token_lab.py, and run it as a regular user. The example reads nothing, changes nothing, and deletes nothing outside this file.

from dataclasses import dataclass class LockBusy(RuntimeError): pass class StaleWriter(RuntimeError): pass @dataclass(frozen=True) class Lease: owner: str token: int expires_at: int class LockService: def __init__(self) -> None: self._counter = 0 self._lease: Lease | None = None def acquire(self, owner: str, now: int, ttl: int) -> Lease: if ttl <= 0: raise ValueError("ttl must be positive") if self._lease is not None and now < self._lease.expires_at: raise LockBusy(f"held by {self._lease.owner}") self._counter += 1 self._lease = Lease(owner, self._counter, now + ttl) return self._lease class NaiveStock: def __init__(self) -> None: self.value = 0 def write(self, value: int) -> None: self.value = value class FencedStock: def __init__(self) -> None: self.value = 0 self.last_token = 0 def write(self, value: int, token: int) -> None: if token < self.last_token: raise StaleWriter( f"token {token} is older than accepted token {self.last_token}" ) self.value = value self.last_token = token locks = LockService() worker_a = locks.acquire("worker-a", now=0, ttl=5) try: locks.acquire("worker-b", now=4, ttl=5) except LockBusy as error: print(f"before expiry: {error}") worker_b = locks.acquire("worker-b", now=6, ttl=5) assert (worker_a.token, worker_b.token) == (1, 2) print(f"new owner: a={worker_a.token} b={worker_b.token}") naive = NaiveStock() naive.write(8) naive.write(5) assert naive.value == 5 print(f"without fence: stock={naive.value}") safe = FencedStock() safe.write(8, worker_b.token) try: safe.write(5, worker_a.token) except StaleWriter as error: print(f"stale write: {error}") assert safe.value == 8 and safe.last_token == 2 safe.write(9, worker_b.token) assert safe.value == 9 and safe.last_token == 2 print(f"with fence: stock={safe.value} last_token={safe.last_token}")

Command run:

python3 fencing_token_lab.py

Output obtained:

before expiry: held by worker-a new owner: a=1 b=2 without fence: stock=5 stale write: token 1 is older than accepted token 2 with fence: stock=9 last_token=2

The process exited with code 0. The assert checks confirm four conditions: the second owner is not allowed before the lease ends, the new owner receives a higher token, the old record indeed overwrites the result in a naive store, and it does not change it in a protected store.

What the run showed

Verification

Without fencing token

With token check

B until t=5

Lock busy

Lock busy

B after t=5

Writes value 8

Writes value 8 with token 2

A resumed at t=7

Late write 5 over 8

Receives StaleWriter

Retry write B

Allowed

Allowed with same token 2

Result

Remaining stock 5 — old value won

Product remaining quantity 9, last_token 2

There is a catch: the token must be checked not only by the blocking service. It must be sent with every protected record, and the storage must atomically compare the token with the last accepted one and save the new value along with the new maximum. If the application first reads last_token and then writes data in a separate request, another owner can intervene between these actions.

Why TTL and owner verification before writing are insufficient

TTL answers the question of when the coordinator can issue a lease to the next worker. It does not revoke already executing code, does not pull a network request out of the buffer, and does not prevent a delayed packet from arriving later. Even an instantaneous check that the lease is still mine does not guarantee that the next record will reach the resource before the new owner's request.

Fencing token moves the decision to the site of the side effect. The resource has already seen token 2, meaning token 1 is stale regardless of the old process's clock and packet delivery order. The number does not have to be a timestamp; it is important that it strictly increases during ownership transfer and does not roll back after a failure or recovery.

What fencing token does not solve

  • A token does not make an operation idempotent. Two commands from the same owner with token 2 are both valid; to protect against reapplying a delivered command, a separate operation_id and a saved result are required.

  • A token does not correct an incorrect SKU, field, price, or product remaining quantity, nor does it replace permission checks for a technical user.

  • If an external API cannot atomically store and compare the token, a number in the log alone does not protect the record.

  • The counter must remain monotonic across restarts, node failures, and restores from backup. Rolling back the counter breaks the guarantee.

  • If a client does not receive unambiguous confirmation that the lease was granted, a separate contract is needed. A client must not consider themselves the owner based solely on a timeout.

  • For multiple independent resources, clear lock boundaries are needed: a global lease for the entire catalog often creates unnecessary queues, while a lock that is too narrow fails to protect the shared invariant.

Retries with the same owner are handled separately in the material on delta, operation_id, and product remaining quantity version. A fencing token answers a different question: which owner is already stale after the lease transfer.

Boundary with 1C-Bitrix and store exchange

In 1C-Bitrix: Site Management, a similar situation is possible for the background import handler, agent, stock update queue, or a custom integration module. However, task names, blocking mechanisms, and available extension points depend on the edition, platform version, module, and specific exchange direction. This example does not verify standard 1C exchange and does not propose inserting a token into product tables without prior design.

  • Clarify exactly what is being protected: the product, product variant, warehouse, exchange package, or order.

  • Fix a stable SKU or product variant identifier, price and stock fields, exchange direction, and the source of the final value.

  • Verify where you can atomically compare the fencing token and write data with the same permissions and within the same transactional boundary.

  • Simulate a pause in the old worker: the new owner must complete the write, and the old one, upon resuming, must receive an explicit rejection, not a silent success.

  • Repeat the current owner's command separately and confirm idempotency via operation_id if the repeat could alter money, quantity, or status.

  • Verify recovery of the coordinator and storage from a backup: the last accepted token must not exceed the new counter.

The readiness criterion here is measurable: after the lease transfer, the new owner writes the value, and any delayed request from the previous owner is rejected directly in the storage. If the system can only show that the TTL has expired but cannot stop the old write, the lock still saves work—data correctness has not yet been entrusted to it.

Discussion 0

Share your experience and ask questions. Comments without links appear after editorial review.

No comments yet. Start the discussion.