<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roopam</title>
    <description>The latest articles on DEV Community by Roopam (@tyroopam9599).</description>
    <link>https://dev.to/tyroopam9599</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3863552%2F2b3a13c9-cc95-486a-8a10-c02fe971d24a.png</url>
      <title>DEV Community: Roopam</title>
      <link>https://dev.to/tyroopam9599</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tyroopam9599"/>
    <language>en</language>
    <item>
      <title>Beyond SETNX: Implementing a Production-Grade Distributed Lock with Node.js and Redis Lua Scripts</title>
      <dc:creator>Roopam</dc:creator>
      <pubDate>Tue, 07 Apr 2026 01:15:12 +0000</pubDate>
      <link>https://dev.to/tyroopam9599/beyond-setnx-implementing-a-production-grade-distributed-lock-with-nodejs-and-redis-lua-scripts-2p1b</link>
      <guid>https://dev.to/tyroopam9599/beyond-setnx-implementing-a-production-grade-distributed-lock-with-nodejs-and-redis-lua-scripts-2p1b</guid>
      <description>&lt;p&gt;Picture this: your restaurant booking platform is growing. You've scaled your Node.js API to four replicas behind a load balancer. Then Friday evening hits, and two guests—Charlie and Diana—both smash "Reserve Table 12" at the exact same millisecond. Both requests land on different instances. Both read the database: &lt;em&gt;table 12 is free&lt;/em&gt;. Both write a booking row. Last writer wins. Charlie gets a confirmation email. Diana gets a confirmation email. Saturday night, both show up. The host is apologetic. The table is awkward. Your on-call engineer is not having a good weekend.&lt;/p&gt;

&lt;p&gt;That is a &lt;strong&gt;double-booking race condition&lt;/strong&gt;, and it's the canonical distributed systems bug for any stateful, time-sensitive resource. Seats on a flight. Stock in a flash sale. A hotel room. A consulting slot.&lt;/p&gt;

&lt;p&gt;A database &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; pessimistic lock would solve this on a single DB node—but only if every booking request hits that same node serially. The moment you have multiple Node.js processes, you need a lock that lives &lt;em&gt;outside&lt;/em&gt; your application layer, in a system that all replicas share. Redis is that system.&lt;/p&gt;

&lt;p&gt;But a naïve Redis lock is actually broken in subtle ways most developers don't discover until it bites them in production. Let me show you why, and how to build a &lt;strong&gt;Distributed Lock&lt;/strong&gt; that's genuinely safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Race Condition Window
&lt;/h2&gt;

&lt;p&gt;Before we get to the solution, let's be precise about the failure mode.&lt;/p&gt;

&lt;p&gt;The old-school approach is a two-step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GET lock:table:12&lt;/code&gt; — check if key exists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SET lock:table:12 "process-A" NX PX 5000&lt;/code&gt; — set it if free&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The problem? Steps 1 and 2 are two separate round-trips to Redis. Between them, the TCP connection yields. Another process—on another server, with its own event loop—can complete its own step 1 before either of you completes step 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Process A         Redis          Process B
    |                |                |
    |--- GET ------&amp;gt; |                |
    | &amp;lt;-- (nil) ---- |                |
    |                |  &amp;lt;-- GET ------|
    |                | --- (nil) ---&amp;gt; |
    |--- SET NX ---&amp;gt; |                |   ← A wins the SET
    | &amp;lt;-- OK ------- |                |
    |--- SET NX ---&amp;gt; |                |   ← B also calls SET NX...
    | &amp;lt;-- OK ------- |                |   ← ...but also gets OK? No—NX prevents this
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait—that scenario is actually safe if you use &lt;code&gt;SET NX&lt;/code&gt; correctly. The real danger is the pattern where developers do the check-then-set in application code, not as a single atomic Redis command. Or when they use &lt;strong&gt;&lt;code&gt;SETNX&lt;/code&gt;&lt;/strong&gt; (the old command) followed by a separate &lt;code&gt;EXPIRE&lt;/code&gt; call. &lt;em&gt;That&lt;/em&gt; gap is exploitable.&lt;/p&gt;

&lt;p&gt;But there's a worse problem lurking further down: &lt;strong&gt;releasing a lock you don't own anymore&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stolen Lock Problem
&lt;/h2&gt;

&lt;p&gt;Here's the scenario that breaks nearly every naïve lock implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t=0ms    Process A acquires lock:table:12, token="uuid-A", TTL=5000ms
t=4800ms Process A is still mid-transaction (slow DB query, GC pause, whatever)
t=5000ms Redis auto-expires the key. Lock is gone.
t=5001ms Process B acquires lock:table:12, token="uuid-B"
t=5100ms Process A finally finishes its transaction. Calls DEL lock:table:12.
         Redis deletes the key—which now belongs to B.
t=5101ms Process C sees the lock is free. Acquires it. Now B *and* C both think
         they hold the lock. Double-booking. Again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;Stolen Lock&lt;/strong&gt; scenario. Process A's &lt;code&gt;DEL&lt;/code&gt; is unconditional—it doesn't check whether it still owns the lock. The &lt;strong&gt;TTL&lt;/strong&gt; saved you from an infinite deadlock, but the unguarded release created a new race.&lt;/p&gt;

&lt;p&gt;The fix requires two things to happen atomically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the current lock value&lt;/li&gt;
&lt;li&gt;Only delete if it matches your token&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And "atomically" here means nothing—no other Redis client command—can interleave between those two steps. That's exactly what &lt;strong&gt;Lua scripts&lt;/strong&gt; give you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Lua? The Atomicity Guarantee
&lt;/h2&gt;

&lt;p&gt;Redis is single-threaded for command execution. Every command is processed one at a time, sequentially, by the Redis event loop. But a sequence of &lt;em&gt;multiple&lt;/em&gt; commands—like a GET followed by a DEL—is not atomic. Another client's command can slip in between them.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Lua&lt;/strong&gt; script executed via &lt;code&gt;EVAL&lt;/code&gt; is different. Redis treats the entire script as a single atomic unit. While the script is running, no other client can execute commands. It's not a lock around Redis itself—it's the guarantee that the script runs to completion without interleaving.&lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://redis.io/docs/latest/develop/programmability/eval-intro/" rel="noopener noreferrer"&gt;Redis docs&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Redis guarantees the script's atomic execution. While executing the script, all server activities are blocked."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is what makes the check-then-delete pattern safe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- RELEASE_LOCK_SCRIPT&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DEL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GET and the DEL happen as one indivisible operation. There is no window. Process A's expired token will never match Process B's live token, so the &lt;code&gt;DEL&lt;/code&gt; is a no-op. Process B's lock survives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
  │  Node.js     │     │  Node.js     │     │  Node.js     │
  │  Replica 1   │     │  Replica 2   │     │  Replica 3   │
  └──────┬───────┘     └──────┬───────┘     └──────┬───────┘
         │                    │                    │
         └────────────────────┼────────────────────┘
                              │
                     ┌────────▼────────┐
                     │     Redis       │
                     │  lock:table:42  │
                     │  → "uuid-A"     │
                     │  TTL: 4812ms    │
                     └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lock lives in Redis. Every replica talks to the same instance. The &lt;strong&gt;Lua&lt;/strong&gt; scripts guarantee that acquire and release are both atomic operations. The &lt;strong&gt;TTL&lt;/strong&gt; is the safety net—if a process crashes mid-transaction, Redis cleans up the orphaned lock automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step Code Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Redis Client
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redis://localhost:6379&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// This shows up in `redis-cli CLIENT LIST` — invaluable when debugging&lt;/span&gt;
  &lt;span class="c1"&gt;// which connections are holding locks in production.&lt;/span&gt;
  &lt;span class="na"&gt;connectionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;table-lock-manager&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;// Cap at 3 retries so integration tests don't stall.&lt;/span&gt;
  &lt;span class="c1"&gt;// In production, tune this based on your latency SLA.&lt;/span&gt;
  &lt;span class="na"&gt;maxRetriesPerRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Emit but don't crash — ioredis will attempt to reconnect.&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[Redis] Connection error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why ioredis and not the official &lt;code&gt;redis&lt;/code&gt; package?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both are fine. I prefer &lt;code&gt;ioredis&lt;/code&gt; for lock managers specifically because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It has a stable, well-documented API for &lt;code&gt;eval()&lt;/code&gt; with explicit &lt;code&gt;numkeys&lt;/code&gt; parameter&lt;/li&gt;
&lt;li&gt;Automatic exponential-backoff reconnection is on by default&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;connectionName&lt;/code&gt; option is built-in (useful for &lt;code&gt;redis-cli CLIENT LIST&lt;/code&gt; during incidents)&lt;/li&gt;
&lt;li&gt;It handles command queuing during reconnection transparently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;maxRetriesPerRequest: 3&lt;/code&gt; cap is deliberate. You don't want a lock acquisition attempt to silently retry for 30 seconds—you want a fast failure so the caller can surface a 503 to the user and they can retry manually.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Key Naming and TTL Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOCK_KEY_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lock:table:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_TTL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOCK_TTL_MS&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The namespace prefix (&lt;code&gt;lock:table:&lt;/code&gt;) is not decoration. Redis is typically shared across features—caching, sessions, rate limiting, pub/sub. A bare key like &lt;code&gt;42&lt;/code&gt; will collide with something eventually. The prefix also makes it trivial to inspect all live locks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli KEYS &lt;span class="s2"&gt;"lock:table:*"&lt;/span&gt;
&lt;span class="c"&gt;# or better, use SCAN in production:&lt;/span&gt;
redis-cli &lt;span class="nt"&gt;--scan&lt;/span&gt; &lt;span class="nt"&gt;--pattern&lt;/span&gt; &lt;span class="s2"&gt;"lock:table:*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TTL from an environment variable matters in production. Your booking transaction time will vary—a fast Postgres query on a warm connection pool might take 50ms; a slow one with retries under load could take 800ms. Ops teams need to tune this without a code deploy. Expose it as a config value.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. The Acquire Script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ACQUIRE_LOCK_SCRIPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  local key   = KEYS[1]
  local token = ARGV[1]
  local ttl   = tonumber(ARGV[2])

  -- SET key token NX PX ttl
  -- NX  → only set if Not eXists
  -- PX  → expiry in milliseconds (never omit this — it's your safety net)
  return redis.call('SET', key, token, 'NX', 'PX', ttl)
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick note here: &lt;code&gt;SET key value NX PX ttl&lt;/code&gt; is actually a single atomic Redis command. You &lt;em&gt;could&lt;/em&gt; skip the Lua wrapper for acquisition and call it directly. I still use Lua here for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: both acquire and release are Lua scripts. The pattern is uniform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensibility&lt;/strong&gt;: if you want to add retry counts, owner metadata, or conditional logic to the acquire path, you do it inside the script without additional round-trips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicitness&lt;/strong&gt;: the Lua version is more readable for anyone who hasn't memorized the &lt;code&gt;SET&lt;/code&gt; command's option flags.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The calling code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;acquireLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;tableId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_TTL_MS&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LockHandle&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;LOCK_KEY_PREFIX&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;tableId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// randomUUID() is crypto-random. Negligible collision probability.&lt;/span&gt;
  &lt;span class="c1"&gt;// It's built into Node's crypto module — no extra dependency.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;ACQUIRE_LOCK_SCRIPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// number of KEYS arguments&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// KEYS[1]&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ARGV[1]&lt;/span&gt;
    &lt;span class="nx"&gt;ttlMs&lt;/span&gt;   &lt;span class="c1"&gt;// ARGV[2]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Lock is held by someone else&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// ... see release section&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why return &lt;code&gt;null&lt;/code&gt; instead of throwing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because lock contention is not an exceptional error—it's an expected, normal outcome in a concurrent system. Throwing forces &lt;code&gt;try/catch&lt;/code&gt; at every call site and conflates "this is bad, something broke" with "this is expected, retry or backoff." Returning &lt;code&gt;null&lt;/code&gt; lets the caller decide: queue the request, return a 409, prompt the user, or implement exponential retry. Keep the control flow clean.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. The Release Script (The Critical Part)
&lt;/h3&gt;

&lt;p&gt;This is the part most implementations get wrong. Read it carefully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RELEASE_LOCK_SCRIPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  local key   = KEYS[1]
  local token = ARGV[1]

  if redis.call('GET', key) == token then
    return redis.call('DEL', key)
  else
    -- We no longer own this lock (expired or stolen). Return 0, not an error.
    return 0
  end
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's trace through the stolen lock scenario again, but with this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t=0ms    Process A acquires lock:table:12, token="uuid-A", TTL=5000ms
t=5000ms Redis auto-expires the key
t=5001ms Process B acquires lock:table:12, token="uuid-B"
t=5100ms Process A finishes, calls release with token="uuid-A"
         Lua script: GET lock:table:12 → "uuid-B"
         "uuid-B" != "uuid-A" → return 0
         B's lock is UNTOUCHED. ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Process A's release is a no-op. Process B continues safely. No race condition.&lt;/p&gt;

&lt;p&gt;The release function returns a &lt;code&gt;boolean&lt;/code&gt;—&lt;code&gt;true&lt;/code&gt; if we released it cleanly, &lt;code&gt;false&lt;/code&gt; if the lock had already expired. That &lt;code&gt;false&lt;/code&gt; case is a &lt;strong&gt;warning, not a crash&lt;/strong&gt;. The table is already unlocked (Redis TTL handled it). What &lt;code&gt;false&lt;/code&gt; tells your ops team is: &lt;em&gt;your TTL is too short for the actual transaction time. Tune it.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;released&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;RELEASE_LOCK_SCRIPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;released&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[Lock] RELEASED — key="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" token="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Lock expired before manual release. Warn ops to tune LOCK_TTL_MS.&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`[Lock] WARN — Lock expired before manual release. `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="s2"&gt;`key="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" token="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5. The &lt;code&gt;finally&lt;/code&gt; Block Is Non-Negotiable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bookTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;tableId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;guestName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;processingDelayMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BookingResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;acquireLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tableId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Table &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tableId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is currently being booked by another request.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;performBookingTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tableId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guestName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;processingDelayMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Table &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tableId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; booked for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;guestName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;".`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ALWAYS release in a finally block.&lt;/span&gt;
    &lt;span class="c1"&gt;// If performBookingTransaction throws, we still release.&lt;/span&gt;
    &lt;span class="c1"&gt;// The TTL is the last line of defence; manual release is the first.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you put &lt;code&gt;lock.release()&lt;/code&gt; in the &lt;code&gt;try&lt;/code&gt; block, an exception from &lt;code&gt;performBookingTransaction&lt;/code&gt; will skip it. The lock stays in Redis until the TTL fires. Every request for that table is rejected for up to 5 seconds. That's bad UX and, at scale, a thundering herd of retries. Use &lt;code&gt;finally&lt;/code&gt;. Always.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure Scenario Simulations
&lt;/h2&gt;

&lt;p&gt;The code includes three concrete scenarios that demonstrate the lock's behavior. Here's what each one proves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 1: Sequential Bookings (Baseline)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;bookTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;T1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// succeeds&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;bookTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;T1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// also succeeds (lock released before Bob tries)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No surprise here. Sequential requests don't contend. Both complete. This is the sanity check that the lock doesn't break the happy path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Concurrent Race — The Real Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;bookTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;T2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Charlie&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 300ms artificial DB delay&lt;/span&gt;
  &lt;span class="nf"&gt;bookTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;T2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Diana&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Promise.all&lt;/code&gt; launches both at the exact same event-loop tick. The &lt;code&gt;processingDelayMs: 300&lt;/code&gt; ensures the first lock-holder is mid-transaction when the second attempt fires, making the contention visible in logs.&lt;/p&gt;

&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Lock] ACQUIRED — key="lock:table:T2"  token="uuid-charlie"
[DB]   Writing booking: table=T2  guest="Charlie"
[Lock] REJECTED — key="lock:table:T2" (lock already held)

Result (Process 1): Table T2 successfully booked for "Charlie".
Result (Process 2): Table T2 is currently being booked by another request.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One winner. One clean rejection. No double-booking. No database write for the loser—the lock prevented it from ever getting to the DB layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: TTL Safety Net — The Crash Simulation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHORT_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1.5 seconds&lt;/span&gt;

&lt;span class="c1"&gt;// Process A acquires but "crashes" — never releases&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;acquireLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;T3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SHORT_TTL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// lock.release() is never called&lt;/span&gt;

&lt;span class="c1"&gt;// Wait past TTL&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHORT_TTL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Process B should now succeed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;bookTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;T3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Eve&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// succeeds ✓&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This validates the TTL as a self-healing mechanism. In a real crash—OOM kill, SIGKILL from a deployment, network partition—the process can't release its lock. Without TTL, the table would be permanently blocked. Redis's key expiration is what makes distributed locks safe to use in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TTL Tuning
&lt;/h3&gt;

&lt;p&gt;Your TTL should be: &lt;code&gt;(p99 transaction time) × safety_multiplier&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If your booking transaction (including DB write and downstream calls) completes in 200ms at p99, a 2000ms TTL gives you 10× headroom. If your p99 is 800ms under load, push it to 5000ms. Monitor &lt;code&gt;[Lock] WARN — Lock expired before manual release&lt;/code&gt; log lines in production—if you see them more than occasionally, your TTL is too short.&lt;/p&gt;

&lt;p&gt;Never set TTL lower than your actual processing time. The whole point of the TTL is to be a &lt;em&gt;safety net&lt;/em&gt;, not a gate that triggers regularly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging and Observability
&lt;/h3&gt;

&lt;p&gt;Every lock acquisition and release should log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key (which resource)&lt;/li&gt;
&lt;li&gt;The token (which process/request)&lt;/li&gt;
&lt;li&gt;The TTL on acquire&lt;/li&gt;
&lt;li&gt;Whether release succeeded or expired&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you a complete audit trail during incidents. When a support ticket says "we double-booked table 7 on Friday at 8:14 PM," you want to be able to reconstruct the exact sequence of lock events from your logs.&lt;/p&gt;

&lt;p&gt;Tag your lock logs with a correlation ID (request ID, trace ID) so you can join them with your application logs in your log aggregator.&lt;/p&gt;

&lt;h3&gt;
  
  
  What About Redis Cluster / Sentinel?
&lt;/h3&gt;

&lt;p&gt;Single-node Redis is sufficient for most applications. If you're running Redis with replication (Sentinel or Cluster), be aware of replication lag: a lock key written to the primary might not yet exist on a replica. If the primary fails before replication completes, a failover could allow two processes to simultaneously acquire what they both believe is the same lock.&lt;/p&gt;

&lt;p&gt;For this specific concern, the &lt;a href="https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/#the-redlock-algorithm" rel="noopener noreferrer"&gt;Redlock algorithm&lt;/a&gt; by Salvatore Sanfilippo addresses it by requiring majority quorum across N independent Redis nodes. However, Redlock comes with significant operational overhead (you need 3-5 Redis instances) and there are well-documented theoretical edge cases around clock drift (see Martin Kleppmann's &lt;a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" rel="noopener noreferrer"&gt;critique&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My opinion:&lt;/strong&gt; if you're running a single Redis primary with &lt;code&gt;appendonly yes&lt;/code&gt; (AOF persistence) and your durability requirements are "don't lose the lock key on restart," Redlock is overkill. The failure window during a Redis primary failover is typically seconds, and the probability of a lock race coinciding exactly with that window is extremely low. A hard-coded fencing token in your DB schema (an incrementing version column) is often a cheaper and more robust solution for true correctness guarantees. Use Redlock only if you have a specific, documented requirement for multi-node lock durability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection Pooling
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ioredis&lt;/code&gt; creates a single TCP connection per &lt;code&gt;new Redis()&lt;/code&gt; instance. For a lock manager that sees high concurrency, you may want to share a single client instance across all requests (as this implementation does with the module-level singleton) rather than creating a new connection per request. The Redis command pipeline handles concurrent EVAL calls correctly—you don't need multiple connections for concurrency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Health Checks
&lt;/h3&gt;

&lt;p&gt;Include Redis connectivity in your application's &lt;code&gt;/health&lt;/code&gt; endpoint. A lock manager that silently fails open (acquiring locks when Redis is unreachable) is arguably worse than failing closed. If &lt;code&gt;redis.ping()&lt;/code&gt; fails, your health check should return 503 so the load balancer stops routing traffic to that instance.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Note on &lt;code&gt;SETNX&lt;/code&gt; vs &lt;code&gt;SET NX&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The title says "Beyond SETNX"—a quick note on why. The original &lt;code&gt;SETNX&lt;/code&gt; command (Set if Not eXists) dates to Redis 1.0. It doesn't accept an expiry argument, so the old pattern was &lt;code&gt;SETNX key value&lt;/code&gt; followed by a separate &lt;code&gt;EXPIRE key seconds&lt;/code&gt;. That gap between &lt;code&gt;SETNX&lt;/code&gt; and &lt;code&gt;EXPIRE&lt;/code&gt; is exploitable—if the process crashes after &lt;code&gt;SETNX&lt;/code&gt; but before &lt;code&gt;EXPIRE&lt;/code&gt;, the key has no TTL and lives forever.&lt;/p&gt;

&lt;p&gt;Redis 2.6.12 added options to the &lt;code&gt;SET&lt;/code&gt; command: &lt;code&gt;NX&lt;/code&gt; (only set if not exists) and &lt;code&gt;PX&lt;/code&gt; (expiry in milliseconds). &lt;code&gt;SET key value NX PX 5000&lt;/code&gt; is atomic. There is no gap. The old &lt;code&gt;SETNX + EXPIRE&lt;/code&gt; pattern is deprecated and should never appear in new code. If you see it in a codebase, replace it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Here's what we built and why each piece matters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lua acquire script&lt;/td&gt;
&lt;td&gt;Atomic SET NX PX&lt;/td&gt;
&lt;td&gt;No race between check and set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID owner token&lt;/td&gt;
&lt;td&gt;Unique per acquisition&lt;/td&gt;
&lt;td&gt;Enables ownership verification on release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lua release script&lt;/td&gt;
&lt;td&gt;GET + conditional DEL (atomic)&lt;/td&gt;
&lt;td&gt;Prevents deleting a lock you no longer own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL on every lock&lt;/td&gt;
&lt;td&gt;Auto-expiry&lt;/td&gt;
&lt;td&gt;Self-healing on process crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;finally&lt;/code&gt; block&lt;/td&gt;
&lt;td&gt;Release always runs&lt;/td&gt;
&lt;td&gt;No orphaned locks from exceptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;null&lt;/code&gt; return on contention&lt;/td&gt;
&lt;td&gt;Structured failure&lt;/td&gt;
&lt;td&gt;Clean call-site control flow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The double-booking problem isn't exotic—it's what happens when you scale any stateful operation to multiple processes without coordination. Redis gives you a shared coordination layer that's fast, battle-tested, and trivial to operate. Lua gives you the atomicity that makes it correct. The owner token is the detail that makes it production-safe.&lt;/p&gt;

&lt;p&gt;The full source is available on GitHub: &lt;strong&gt;&lt;a href="https://github.com/TyRoopam9599/distributed-lock-redis-lua" rel="noopener noreferrer"&gt;TyRoopam9599/distributed-lock-redis-lua&lt;/a&gt;&lt;/strong&gt;. Run &lt;code&gt;npx ts-node src/lockManager.ts&lt;/code&gt; against a local Redis instance to see all three scenarios play out in your terminal.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you run into the stolen-lock scenario in production? Or do you have a different take on Redlock vs. single-node? Drop a comment below — I'd like to hear how others have approached this.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>redis</category>
      <category>backend</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
