Skip to content

Redis - Scripting

Redis allows for the execution of server-side scripts using a built-in Lua interpreter (available since Redis 2.6.0). The primary and most powerful benefit of Redis scripting is atomicity. A script is always executed as a single, uninterruptible operation. No other command can run concurrently while a script is executing, ensuring data consistency for complex operations.

  • Atomicity: Perform complex, multi-step operations (like ‘read-then-write’) as a single atomic transaction without needing to use MULTI/EXEC.
  • Performance: Reduce network latency by combining multiple commands into a single round trip between your client and the Redis server.
  • Data Locality: Logic executes on the server where the data lives, which can be more efficient than fetching data to the client, processing it, and sending it back.

You can execute scripts using the EVAL command. A better practice for performance is to use SCRIPT LOAD to cache the script on the server, which returns a SHA1 hash. You can then execute the cached script using EVALSHA and its hash, saving network bandwidth on subsequent calls.

EVAL script numkeys key [key ...] arg [arg ...]
  • script: The Lua script body as a string.
  • numkeys: The number of key names being passed to the script (from the arguments that follow).
  • key [key …]: The names of the keys your script will access. These are available in Lua via the KEYS table (e.g., KEYS[1], KEYS[2]).
  • arg [arg …]: Additional arguments for your script. These are available in Lua via the ARGV table (e.g., ARGV[1], ARGV[2]).

Let’s implement a simple rate limiter that allows a certain number of actions per user within a time window. This requires an atomic ‘increment and set-expiry’ operation, which is a perfect use case for a Lua script.

-- The Lua Script --
-- KEYS[1]: The key for the user (e.g., 'rate_limit:user123')
-- ARGV[1]: The expiration time window in seconds
local current = redis.call('INCR', KEYS[1])
if tonumber(current) == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current

Executing the script:

# First request for user123, with a 60 second window.
# The script increments the count to 1 and sets the expiry.
redis> EVAL "local current = redis.call('INCR', KEYS[1])\nif tonumber(current) == 1 then\n redis.call('EXPIRE', KEYS[1], ARGV[1])\nend\nreturn current" 1 rate_limit:user123 60
(integer) 1
# Second request within 60 seconds. The count is now 2.
redis> EVAL "... same script ..." 1 rate_limit:user123 60
(integer) 2
# The application logic would then check if the returned count exceeds the limit (e.g., > 10).
  • Prefer EVALSHA: Always try to cache your scripts with SCRIPT LOAD and execute them with EVALSHA. Most client libraries handle this automatically.
  • Keep Scripts Fast: Scripts are blocking. A long-running script will stall your entire Redis server. Avoid complex loops or CPU-intensive logic.
  • Keys vs. ARGV: Always pass key names via the KEYS table, not ARGV. This is crucial for Redis Cluster compatibility and future analysis tools.
  • Redis Functions (Redis 7+): Redis 7 introduced Functions, a more advanced way to manage scripts. Functions are grouped into libraries, are persisted, and are replicated across your cluster, providing a more robust and manageable alternative to standalone EVAL scripts.
CommandDescription
EVAL script numkeys key [key …] arg [arg …]Executes a Lua script.
EVALSHA sha1 numkeys key [key …] arg [arg …]Executes a Lua script that is already cached by its SHA1 hash.
SCRIPT LOAD scriptLoads a script into the script cache and returns its SHA1 hash.
SCRIPT EXISTS sha1 [sha1 …]Checks for the existence of one or more scripts in the cache.
SCRIPT FLUSH [ASYNC|SYNC]Removes all scripts from the script cache.
SCRIPT KILLKills the script currently in execution. Can only be used on scripts that haven’t performed any write operations.
FUNCTION LOAD [REPLACE] function-codeLoads a library of functions (Redis 7+).
FCALL function numkeys [key [key …]] [arg [arg …]]Invokes a function from a loaded library (Redis 7+).