How to Implement Distributed Locking in ASP.NET Core 10 Using Redis and C# (2026)
A Redis lock is a time-limited lease. A worker obtains it by setting a key only when absent and storing a unique owner token with an expiration. Release must compare that token with the current value before deleting the key. This pattern can reduce duplicate work across ASP.NET Core instances. Correctness-critical writes still need a safeguard at the system receiving the write.
Why an in-process lock is insufficient
Two API replicas have separate memory. An in-process semaphore can prevent overlap on one process while both replicas independently rebuild the same cache entry. A shared lease gives the replicas a common place to decide who starts. It helps when rebuilding is expensive and repeating the work is tolerable.
The lease lifetime bounds ownership at Redis. It does not bound how long a process may pause. A paused worker can resume after expiration while a new worker already owns the key. A retry or failover can introduce additional ambiguity. Design the downstream operation with these cases in mind. Redis distributed locking guidance
Configure a local test
Install .NET 10 and Redis. Start Redis bound to loopback for a local lab with docker run --rm -p 127.0.0.1:6379:6379 redis:8. Create a minimal web project and add the StackExchange.Redis package. Keep one multiplexer per process rather than opening a new connection on each request. Set ConnectionStrings__Redis for both replicas. The sample is for direct loopback testing. Authenticate and authorize any deployed trigger.
Acquire and release the lease
The sample uses LockTakeAsync and LockReleaseAsync from StackExchange.Redis. On the wire the key must be set conditionally with an expiry, and release must check ownership atomically. A separate GET and DELETE introduces a race because ownership may change between them. The GUID is an owner token. It does not provide ordering.
If Redis responds with an error during acquisition, the endpoint returns 503 and avoids running the work. A timeout can leave the result of a command uncertain. A TTL lets an orphaned lease disappear. An unsuccessful release attempt should be observable and should never delete another owner’s lock. Cancellation of the simulated work passes through the release path.
Reproduce contention
Run two instances against the same Redis, on loopback ports 5111 and 5112. Send these requests close together.
While the first request holds the lease, expect one 200 and one 409. The ordering can vary. Requests for separate IDs do not contend on the same key. Stop Redis before a test and expect a dependency failure once the connection is established and commands fail, or a startup error if the initial connection cannot be created. Restore Redis and verify normal behavior. In a local Redis 8 smoke test, two simultaneous requests for the same ID returned 409 and 200. After release, a third request returned 200. Failover and extended pauses were not exercised.
Renewal and fencing
Choose a lease long enough for the normal work with margin. For longer jobs, StackExchange.Redis exposes LockExtendAsync with the same key and owner value. Renew well before expiration. A renewal failure means the worker must assume its lease may be lost. Stopping work promptly reduces overlap, but cancellation cannot revoke a write that was already sent.
A fencing token is an increasing sequence number that the resource receiving a write can check. If it has accepted token 42, it can reject a later-arriving write carrying token 41. Generating an increasing number is only half the design. The destination must enforce it atomically with the write, and the numbering system must retain its ordering guarantees during failure. A random owner token does not provide that guarantee.
| Need | Candidate |
|---|---|
| Reduce redundant cache rebuilds | Short Redis lease with safe release. |
| Prevent duplicate invoice creation | Unique database constraint and transactional idempotency. |
| Reject late writes from expired workers | Destination-enforced version or fencing token. |
| Coordinate durable business state | Database transaction or a coordination service matched to the fault model. |
See the ASP.NET PB idempotency keys guide for a database-backed duplicate-request example.
Troubleshooting and FAQs
Why do two workers sometimes run the task? Check whether work exceeded the TTL or Redis failed over. Also check whether replicas use the same Redis namespace and key format.
Should I retry 409 immediately? Use a bounded delay with jitter when the caller can retry, or return current job state. Immediate retries across many callers amplify contention.
Does a replica promote the lease safely? Asynchronous replication can lose a recently written lease during failover. A simple primary-based lease cannot eliminate this failure mode.
Should the app continue when Redis is unavailable? This endpoint fails closed. For an optional cache optimization, another policy may be acceptable if duplicate work is explicitly tolerated.
The important question is what happens after an old worker resumes. A well-designed receiver can recognize and reject stale work even when a coordinator has timed out.
Comments
Post a Comment