Valkey Lua scripting lets you run multiple commands as a single, indivisible operation, guaranteeing that either all commands succeed, or none of them do.

Let’s see it in action. Imagine you need to atomically increment a counter and add a new member to a set. Doing this with separate INCR and SADD commands leaves a window where the counter might be incremented but the set addition fails, or vice-versa.

-- atomically_increment_and_sadd.lua
local counter_key = KEYS[1]
local set_key = KEYS[2]
local increment_amount = ARGV[1]
local set_member = ARGV[2]

-- Increment the counter
local new_counter_value = redis.call('INCRBY', counter_key, increment_amount)

-- Add the member to the set
local add_result = redis.call('SADD', set_key, set_member)

-- Return the results
return {new_counter_value, add_result}

To execute this script using valkey-cli:

valkey-cli --eval atomically_increment_and_sadd.lua mycounter myusers 5 'user123' 2>/dev/null

This command will:

  1. eval: Execute the Lua script.
  2. atomically_increment_and_sadd.lua: The path to your script file.
  3. mycounter myusers: These are the KEYS arguments passed to the script. KEYS[1] will be mycounter, and KEYS[2] will be myusers.
  4. 5 'user123': These are the ARGV arguments. ARGV[1] will be 5, and ARGV[2] will be 'user123'.
  5. 2>/dev/null: This redirects any potential Valkey errors to /dev/null for cleaner output in this example.

The output you’d see might look like this:

1) "10"
2) "1"

Here, 10 is the new value of mycounter after being incremented by 5 (assuming it started at 5), and 1 indicates that user123 was successfully added to the myusers set (since SADD returns 1 if the member was added, and 0 if it was already present).

The core problem Valkey Lua scripting solves is race conditions. In a distributed system or a concurrent application, multiple clients might try to modify the same data simultaneously. Without atomicity, operations can interleave in unexpected ways, leading to inconsistent states. For instance, if client A reads a value, client B modifies it, and then client A writes its modified value back, client B’s change is lost. Lua scripts in Valkey are executed on the server side as a single atomic unit. The Valkey server guarantees that once a script begins execution, no other commands from any client will be interleaved until the script completes. This isolation is crucial for maintaining data integrity.

The redis.call() function is your gateway to executing native Valkey commands from within the Lua script. It takes the command name as a string, followed by its arguments. These arguments can be literal values or come from the KEYS and ARGV tables passed to the script. The KEYS table is intended for keys that the script will operate on, and the ARGV table is for other arguments like values, counts, or options. This separation helps Valkey potentially optimize key-related operations.

When you execute a Lua script, Valkey first checks if it has already processed and cached that script. If it has, Valkey uses the cached version, which is faster. If not, Valkey loads the script into memory, assigns it a SHA1 hash, and executes it. Subsequent calls with the same script can then use the EVALSHA command with the script’s hash, avoiding the overhead of sending the script text again.

The most surprising thing about Valkey Lua scripting is how it leverages the Lua interpreter embedded directly within the Valkey server process. This means your scripts aren’t sent over the network for execution; they run directly where the data lives. This co-location dramatically reduces latency and eliminates the possibility of network issues corrupting your multi-command transactions. The entire script, from start to finish, is a single, uninterruptible event from the perspective of other clients.

The KEYS and ARGV structure is more than just a convention; it’s a mechanism for Valkey to understand which keys are being accessed. This allows Valkey, especially in clustered environments, to route commands appropriately and manage key distribution. While you can reference keys directly within redis.call() inside your script (e.g., redis.call('SET', 'mykey', 'somevalue')), it’s considered best practice to pass them via KEYS for better compatibility and potential optimizations.

You might encounter errors if your Lua script itself has syntax errors, or if the Valkey commands called within the script fail. Valkey will return an error to the client indicating this. For instance, trying to INCRBY a key that holds a string will result in a WRONGTYPE error, which will be surfaced by your script execution. Error handling within Lua scripts can be done using pcall (protected call) if you need to gracefully handle potential command failures without the entire script aborting.

The next logical step after mastering atomic multi-command operations is understanding how to manage and replicate Lua scripts effectively, especially in production environments where script caching and distribution become critical.

Want structured learning?

Take the full Valkey course →