← Writing

Atomic Operations in Redis with Lua Scripting

The Atomicity Problem

MULTI/EXEC gives you transactions, but with caveats:

> MULTI
> SET user:1:balance 100
> GET user:1:balance
> EXEC

If another client writes between GET and EXEC, your transaction doesn't "abort" — you just get the old value. This is Watch-Exec semantics, not true ACID.

Lua scripts are different: they run atomically at the Redis server level, with zero interleaving.

Hello, Lua

A Lua script that increments a key and returns its new value:

-- KEYS[1] is the key name
-- ARGV[1] is the increment amount
local current = redis.call('GET', KEYS[1])
local new = tonumber(current) + tonumber(ARGV[1])
redis.call('SET', KEYS[1], new)
return new

From Node.js (using ioredis):

const result = await redis.eval(script, 1, 'counter', 5);
// args: script, numKeys, key1, arg1
// returns: new value after +5

Real-World: Distributed Lock

A safe lock acquire with TTL:

-- KEYS[1] = lock name
-- ARGV[1] = random token
-- ARGV[2] = TTL in milliseconds

if redis.call('EXISTS', KEYS[1]) == 0 then
  redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
  return 1  -- lock acquired
else
  return 0  -- lock exists
end

Release with token check:

-- KEYS[1] = lock name
-- ARGV[1] = random token

if redis.call('GET', KEYS[1]) == ARGV[1] then
  redis.call('DEL', KEYS[1])
  return 1  -- lock released
else
  return 0  -- token mismatch (not our lock)
end

The token prevents you from deleting a lock acquired by another client.

Limits

  • Scripts have 5-second timeout by default (configurable)
  • Can't do blocking operations (BLPOP, etc.)
  • Lua 5.1 (older dialect, no table.unpack)
  • No access to time; use Redis CLOCK command

For most use cases, these limits don't matter. Script execution is nanoseconds.