A community member asked how TTL renewal works for NATS Key/Value (KV), especially when KV entries are used as distributed lease locks.
There is not a single universal “renew TTL” operation for every KV TTL use case.
Avoid using unconditional writes for locks. A lock renewal should normally be conditional on the revision you already own, so you do not overwrite another process’s lock after yours has expired.
NATS KV is built on JetStream. A KV key is represented by messages in a stream, and a key’s current value is the latest revision for that key. Expiration is ultimately stream retention/age behavior, not a separate timer attached to an external database row.
That implementation detail matters for locks:
PUT, DELETE, or PURGE.PUT, and an expiration can be delivered as a PURGE/delete. This distinction is not automatic. It requires the bucket (or its underlying stream) to be configured to write delete markers when entries are removed by age. Without that feature, expiration is silent — a running watcher receives no event, and a watcher that starts afterward simply never sees the key.If all entries in the bucket can share the same expiration policy, configure TTL/MaxAge at the bucket level. Then a lease holder can periodically update the key before it expires.
A typical shape is:
Pseudo-code:
created = kv.create("locks/job-a", owner_id)revision = created.revision
while still_working: sleep(renew_interval)
updated = kv.update("locks/job-a", owner_id, expected_revision=revision) if updated failed: stop_work_or_reconcile() break
revision = updated.revisionThe important part is the compare-and-set behavior. A plain unconditional put may be unsafe for a lock because it can recreate or overwrite a key after another instance has acquired it.
Choose a renew interval comfortably shorter than the bucket TTL. For example, if the TTL is 30 seconds, renewing every 10 seconds gives more room for scheduling delays, network hiccups, and retry logic. The exact values depend on your workload and failure model.
Some designs want different TTLs per lock entry. In that case, “renew the TTL” may mean “write the same key again with a new per-message TTL.” Depending on the client API and TTL feature being used, the KV API may not provide a direct operation for that.
One possible advanced approach is to work with the underlying JetStream stream semantics directly:
This is more powerful, but it is also more coupled to JetStream details and to the exact TTL support available in your server/client combination. It should be tested carefully before being used as a locking primitive.
Watchers should not treat every event the same way.
A renewal/update is a write event. Expiration is a removal event. These are only observable as separate events when the bucket or its underlying stream is configured to emit delete markers (also called subject delete markers or limit markers), a feature of recent NATS server versions. With delete markers enabled, a JetStream-level consumer sees an expiration caused by MaxAge as a marker message carrying a Nats-Marker-Reason: MaxAge header, and a KV watcher sees the same removal as a PURGE/delete operation.
If delete markers are not enabled, expiration produces no event at all, and a watcher cannot tell “the lease expired” apart from “the key was never set.” If your handoff logic depends on observing expiration, confirm that delete markers are enabled on the lock bucket.
That distinction lets another process tell the difference between:
Even with that distinction, lock handoff code should still use create/update expectations. Watch notifications are useful signals, but the write that acquires or renews the lock is what must enforce ownership.
For many applications, the simplest robust design is:
This design accepts that, under failures or long pauses, another instance may acquire the lock. That is usually a better tradeoff than adding a second lock or a complex renewal protocol, unless the application truly requires stricter coordination.
KV can be a good fit for lightweight leases, but be careful if your design requires:
In those cases, consider adding fencing tokens, making the downstream operation compare the lock revision, or using a coordination mechanism specifically designed for your consistency requirements.
For bucket-level NATS KV TTL, a conditional update creates a fresh revision and is the usual way to refresh a lease. For custom per-entry TTLs, the high-level KV API may not provide the renewal semantics you want, so you either need lower-level JetStream usage or a design that tolerates expiration races. In all cases, use compare-and-set style operations for locks and treat renewal failure as loss of ownership.
Want help from the NATS experts? Meet with our architects to get help tailored to your use case and environment.
News and content from across the community