Locks
A lock gives one client exclusive access to a named resource. Locks are built on leases, so a lock cannot be held forever by a crashed process — when the backing lease expires, the lock releases automatically.
Why use locks
When several instances might run the same critical operation at once, uncoordinated execution causes duplicate processing, data corruption, or inconsistent state. A lock ensures that only one instance runs the operation at a time.
How a lock works
Acquiring a lock makes you the exclusive owner of that lock name. While you hold it, no other client can acquire the same lock. The lock is backed by a lease, so its lifetime is the lease's lifetime.
A useful way to hold this in your head:
Lock = exclusive access + lease-based ownership
Because the lease controls the lifetime, a lock inherits the lease's failure safety: if you stop renewing, the lock releases on its own.
Acquire a lock
Call AcquireAsync with a lock name and a duration. The duration is the lock's time-to-live: it stays held for that window unless you release or renew it. A backing lease is created and managed for you — you do not grant one yourself.
var lockHandle = await client.Locks.AcquireAsync(
"lock:order:1001",
TimeSpan.FromSeconds(10));
AcquireAsync does not block until the lock is free. It returns a lock handle if you acquired the lock, or null if another client already holds it. Always check the handle for null before entering the protected section.
Do not assume AcquireAsync succeeded. If another client already holds the lock, AcquireAsync returns null and you did not acquire anything. Running your critical section without the null check defeats the lock and lets two instances proceed at once.
Release a lock
A lock releases automatically when its time-to-live expires without renewal. When a worker finishes cleanly, release it explicitly through the handle instead of waiting for expiry.
await lockHandle.ReleaseAsync();
Example: prevent duplicate processing
This pattern ensures only one worker processes a job. The null check gates the work, and releasing the handle frees the lock when the work is done.
var lockHandle = await client.Locks.AcquireAsync("lock:job:1", TimeSpan.FromSeconds(10));
if (lockHandle is not null)
{
await client.PutAsync("job:1", "processing");
// do work here
await lockHandle.ReleaseAsync();
}
Another worker that tries to acquire lock:job:1 gets null until the lock is released.
Behavior on failure
If the process crashes while holding the lock, it stops renewing. The backing lease expires and the lock releases automatically, so another worker can proceed. This is why a lease-backed lock does not deadlock the system when an owner dies.
Lock lifetime is the time-to-live
The duration you pass to AcquireAsync is the lock's lifetime. While you renew it the lock is held, and when the time-to-live lapses without renewal the lock expires.
A lock is only valid until its time-to-live elapses. For work that runs longer than that window, you must renew the lock with lockHandle.RenewAsync(...) while you hold it. Renewal returns true if it succeeded and false if the lock was already lost. If the lock expires mid-operation, it silently releases and another client can acquire it while you are still working — the two of you then run the critical section at the same time. Size the duration to your expected work time and renew for anything longer.
await lockHandle.RenewAsync(TimeSpan.FromSeconds(10));
Best practices
Follow these practices to keep lock behavior predictable.
- Use a descriptive lock key, such as
lock:order:1001, so the resource being protected is obvious. - Match the time-to-live to the expected work time.
- Renew the lock for long-running operations instead of acquiring with an oversized time-to-live up front.
- Hold locks for as short a time as possible to reduce contention.
When to use locks
Use a lock when only one instance should perform an operation, when you need to protect a shared resource from concurrent access, or when you must prevent duplicate execution of the same work.