A Redis lock prevents parallel execution. It does not guarantee correctness.
Under load, expiry races, GC pauses, and network partitions can still produce duplicate execution. And duplicates are correctness failures.
If I hold the lock, no one else runs this code.
That assumption is false in distributed systems.
A lock is a coordination primitive. Correctness is a data guarantee.
var acquired = await redis.StringSetAsync(
"lock:order:123",
nodeId,
TimeSpan.FromSeconds(10),
When.NotExists
);
if (!acquired)
return;
await ProcessOrder();
await redis.KeyDeleteAsync("lock:order:123");
Looks safe.
It is not.
Timeline:
T0 Node A acquires lock (TTL = 10s)
T8 Node A still processing
T10 Lock expires
T10.1 Node B acquires lock
T11 Node A completes
→ Double execution
The system behaved exactly as configured. No component failed.
The failure is logical, not technical.
In .NET, stop-the-world garbage collection can pause threads.
During a Gen2 GC:
If GC pause = 400ms–800ms under pressure, expiry windows shrink dramatically.
Reference: .NET GC Fundamentals
Consider temporary network partition:
Node A acquires lock
Network isolates Node A from Redis
TTL expires
Node B acquires lock
Node A continues processing (still alive)
Both nodes believe they are correct.
This is split-brain behavior.
Distributed systems cannot assume perfect connectivity.
Suppose:
If even 2% of executions exceed TTL due to variance:
5,000 × 2% = 100 potential duplicate windows per second
Over one hour:
100 × 3600 = 360,000 potential duplicate events
Even if only 0.1% materialize:
360,000 × 0.1% = 360 real duplicates per hour
Locks reduce concurrency. They do not eliminate race probability.
Redis RedLock attempts to solve this using quorum across multiple nodes.
However, it remains controversial.
Martin Kleppmann critique: How to do distributed locking
Redis author response: Redis Distributed Locks
The debate centers around:
Even RedLock cannot give absolute correctness under asynchronous networks.
The real solution is idempotency at the data layer.
INSERT INTO payments (idempotency_key, amount, ...)
VALUES (@key, @amount, ...)
ON CONFLICT (idempotency_key)
DO NOTHING;
Or enforce uniqueness constraint:
CREATE UNIQUE INDEX ux_payment_idempotency
ON payments(idempotency_key);
Locking prevents parallelism. Idempotency prevents duplication.
They solve different problems.
Concurrency control belongs to the database. Not to Redis.