Redis Lua scripts let you run multiple commands on the server as a single, atomic operation.

Here’s a Redis Lua script that increments a counter and adds a timestamp to a sorted set, all in one go:

local counterKey = KEYS[1]
local timestampSetKey = KEYS[2]
local timestamp = ARGV[1]

redis.call('INCR', counterKey)
redis.call('ZADD', timestampSetKey, timestamp, timestamp)

return {redis.call('GET', counterKey), redis.call('ZCARD', timestampSetKey)}

When you execute this script using redis-cli, you’ll pass the keys and arguments like this:

redis-cli --eval your_script.lua counter_key,timestamp_set_key , 1678886400

The output will look something like this:

1) "123"
2) "45"

This demonstrates the script incrementing counter_key to "123" and adding a new entry to timestamp_set_key, resulting in a cardinailty of "45".

The core problem Redis Lua scripts solve is the race condition inherent in executing multiple commands sequentially from a client. Imagine you have a Redis instance and two clients. Client A needs to increment a counter and then add an element to a sorted set. Client B also needs to do the same. Without Lua scripting, Client A might increment the counter, but before it can add to the sorted set, Client B might also increment the counter. This leads to an incorrect final count. Lua scripts execute within Redis as a single, uninterruptible command. The EVAL command in Redis guarantees atomicity for the script’s execution.

Internally, Redis treats a Lua script execution as a special command. When you send EVAL with your script and arguments, Redis loads the script into its Lua interpreter. The interpreter runs the script directly on the server, interacting with Redis’s data structures via the redis.call() function. This avoids network round trips between the client and server for each individual Redis command within the script. The KEYS and ARGV tables are how you pass external data into your script. KEYS is for keys that your script operates on, and ARGV is for other arguments.

The beauty of Lua scripting lies in its simplicity and power. You can define complex logic, perform conditional operations, and even loop within the script, all while maintaining atomicity. This is incredibly useful for operations that logically belong together, like updating an inventory count and logging the transaction, or managing user sessions where you might update a last-seen timestamp and increment a login counter.

The redis.call() function is your gateway to interacting with Redis from within the Lua script. It mirrors the Redis command syntax. For instance, redis.call('INCR', counterKey) is equivalent to running INCR counter_key on redis-cli. Similarly, redis.call('ZADD', timestampSetKey, timestamp, timestamp) maps to ZADD timestamp_set_key timestamp timestamp. The first timestamp in ZADD is the score, and the second is the member. Here, we’re using the same value for both, which is common when you just need a sorted list of timestamps.

The return value of your Lua script is sent back to the client as a single response. This can be a single value, a list of values, or nil. In our example, we return a table containing the new counter value and the cardinailty of the timestamp set. Redis marshals these Lua return types into its own protocol for sending back to the client. For tables, it typically uses the bulk string reply or multi bulk reply depending on the contents.

A common pattern is to use redis.call('GETSET', key, value) within a script. This command atomically sets a key to a new value and returns the old value. It’s incredibly useful for implementing locks or tracking state transitions where you need to know the previous state immediately after changing it, all without a separate GET operation that could be interrupted.

The next step is to explore how to manage and load these Lua scripts efficiently using EVALSHA to avoid resending the script itself on every execution.

Want structured learning?

Take the full Redis course →