Redis Functions vs. Classic Lua Scripting: Migration in Practice
AI generated
SET
TTL
Redis · Scripting · Functions
Redis Functions vs. Classic Lua Scripting
migration in practice

Classic Lua scripting with EVAL and EVALSHA has been the standard way to run atomic server logic in Redis for years, but it brings practical problems around maintainability, versioning, and replication behavior. Since Redis 7, Redis Functions offer a more structured alternative through FUNCTION LOAD, treating scripts as named, persistently stored libraries instead of ephemeral, hash-referenced individual scripts. What the switch looks like in practice, what advantages it brings, and where EVAL still remains the better choice, is what this article covers.

12 min read FUNCTION LOAD · EVAL Redis 7 · Redis 8 · Lua

1. Why classic EVAL hits limits in practice

A Lua script executed via EVAL initially exists for Redis merely as an anonymous block of text, which would have to be sent along with every call, were it not for server-side script caching via SCRIPT LOAD and the subsequent invocation by SHA1 hash with EVALSHA. This model works, but carries a structural problem: the hash is a pure identifier with no semantic meaning, so application code usually has to keep its own bookkeeping of which hash belongs to which script, and after a server restart or SCRIPT FLUSH, all scripts must be reloaded before EVALSHA works again.

There is also no built-in versioning: if a script changes, its hash changes too, and old hashes still in circulation become invalid, without Redis itself keeping any overview of which script versions are actually in use at a given time. For a handful of small, rarely changed scripts this is not much of a problem, but for a growing collection of long-lived server logic it quickly becomes unwieldy.

2. The basic principle of Redis Functions: named libraries instead of anonymous scripts

Redis Functions flip this model around: instead of individual, hash-referenced scripts, FUNCTION LOAD loads a complete, named library containing one or more named functions. Each function is called by its name, for instance with FCALL myfunction 1 mykey, not via a hash computed from the script content. The library name itself is part of Redis's persistent configuration and thus survives restarts, without the application having to keep its own bookkeeping of hashes.

Technically, Lua remains the scripting language; Functions are not a new programming language, but a more structured wrapper around the same Lua execution environment, complemented by a fixed registration scheme via redis.register_function() within the library definition.


#!lua name=cart_library

redis.register_function('cart_add_item', function(keys, args)
    local cart_key = keys[1]
    local item_id = args[1]
    local qty = tonumber(args[2])
    redis.call('HINCRBY', cart_key, item_id, qty)
    redis.call('EXPIRE', cart_key, 3600)
    return redis.call('HGETALL', cart_key)
end)

3. Loading a library and calling functions

Loading a library happens via FUNCTION LOAD, followed by the complete library source, which must start with a shebang line #!lua name=libraryname. This name binding is mandatory and ensures Redis immediately detects naming conflicts between different libraries, instead of letting them surface later as hard-to-diagnose runtime errors.

Calling a registered function happens via FCALL or, for pure read functions without write access, via FCALL_RO, each with the function name, the number of keys passed, the keys themselves, and any additional arguments. This explicit separation between keys and other arguments matches the familiar pattern from EVAL, so existing call conventions carry over largely unchanged.


# Load a library from a file
redis-cli -x FUNCTION LOAD < cart_library.lua

# Call a registered function: 1 key, two arguments
FCALL cart_add_item 1 cart:4711 sku-123 2

# List all loaded libraries and their functions
FUNCTION LIST

4. Maintainability advantage: named libraries instead of a hash zoo

The central maintainability advantage of Functions is that a library gets swapped as a whole: FUNCTION LOAD REPLACE fully replaces an existing library with a new version, while every function name it contains stays stable as long as it is still defined in the new version. Application code that calls functions by name therefore does not need to change on an update, unlike EVALSHA calls, which must reference a new hash after every script change.

In addition, the complete library source can be backed up at any time via FUNCTION DUMP and restored via FUNCTION RESTORE, enabling clean version management outside Redis, for instance as part of a deployment repository where every library version exists as its own traceable file, instead of a collection of hard-to-attribute hash values.

5. Replication advantage: effect replication instead of script propagation

With classic EVAL, Redis by default propagates the actually executed, resulting write commands to replicas and to the AOF, not the script call itself, as long as the script is marked deterministic. For non-deterministic scripts, such as ones accessing TIME or RANDOMKEY, the script author must explicitly switch to effect replication via redis.replicate_commands(), which is often forgotten in older scripts and can then lead to inconsistencies between primary and replica instances.

Redis Functions enforce effect replication by default from the outset, without the function author having to worry about it explicitly. Every write operation executed within a Function is propagated to replicas individually and deterministically, structurally eliminating the risk of silent inconsistencies from forgotten replication instructions, rather than leaving it to the script author's discipline.

6. Practical migration path: converting an existing EVAL script

Migrating an existing EVAL script starts with identifying all scripts in production use, usually via SCRIPT LIST or an inventory in the application code, since Redis itself keeps no description of what a script is meant for. Next, the existing Lua code gets wrapped in a function definition with a shebang line and a redis.register_function() call, where the actual logic can usually be carried over unchanged, since the underlying Lua execution environment stays the same.

The application is then prepared to call both paths in parallel, calling both EVALSHA and FCALL on a trial basis, comparing results in a staging environment, and only switching fully to FCALL after successful verification. Old, no-longer-needed EVAL-based scripts can then be removed from the application code step by step, without requiring a hard cutover point.


-- Before: classic EVAL script (atomic counter with limit)
-- KEYS[1] = counter key, ARGV[1] = max value
if tonumber(redis.call('GET', KEYS[1]) or '0') >= tonumber(ARGV[1]) then
    return 0
end
return redis.call('INCR', KEYS[1])

-- After: the same logic as a registered function
#!lua name=counter_library

redis.register_function('counter_incr_limited', function(keys, args)
    local current = tonumber(redis.call('GET', keys[1]) or '0')
    if current >= tonumber(args[1]) then
        return 0
    end
    return redis.call('INCR', keys[1])
end)

7. When EVAL still remains the better choice

For one-off, ad-hoc scripts, such as a manual data migration run as part of a single maintenance action or a debugging script that only runs temporarily during an incident investigation, the extra overhead of a named, registered library is disproportionate. Here, EVAL with the script content sent directly remains the more pragmatic choice, since no persistent registration or library cleanup is required.

In very old Redis versions before 7.0, still running in some production environments for compatibility reasons, FUNCTION LOAD simply is not available, so EVAL remains the only option there for lack of an alternative. When a Redis 7 or newer upgrade is planned, it is worth deliberately deciding which existing scripts genuinely benefit from migrating to Functions and which are better left as one-off tools on EVAL.

8. Extra control: flags for read access and timing behavior

When registering a function, additional properties can be declared through a flags table, such as no-writes for functions guaranteed not to execute any write commands and therefore safely callable via FCALL_RO on replicas, or allow-stale for functions that can run meaningfully even on a replica with stale data. These flags are enforced by Redis itself; a function call carrying the no-writes flag that nevertheless contains a write command is rejected with an error.

Classic EVAL has no comparable declarative safeguard: whether a script only reads is something the caller must know and ensure by convention, without Redis technically enforcing it. This extra layer of control is another, often underestimated maintainability advantage of Functions over EVAL, especially in teams with multiple developers maintaining the same library.


#!lua name=readonly_library

redis.register_function{
    function_name='cart_get_total',
    callback=function(keys, args)
        local items = redis.call('HGETALL', keys[1])
        local total = 0
        for i = 2, #items, 2 do
            total = total + tonumber(items[i])
        end
        return total
    end,
    flags={'no-writes'}
}

9. Operational management: listing, deleting, and persisting libraries

Loaded libraries are treated as persistent by default: they survive a restart as long as RDB or AOF persistence is active, and are automatically transferred to replicas during replication, without the application having to load them there again. FUNCTION LIST WITHCODE retrieves the complete source of all loaded libraries along with metadata such as function names and flags, which works well for automated consistency checks between the expected and the actually loaded version.

Libraries no longer needed can be removed specifically with FUNCTION DELETE libraryname without affecting other libraries, while FUNCTION FLUSH deletes all loaded libraries at once and should therefore only be used deliberately and rarely, for instance as part of a controlled rollback.

Aspect Classic EVAL/EVALSHA Redis Functions Practical relevance
Identification SHA1 hash of the script content Named library and function Functions need no manual hash bookkeeping
Update behavior New hash on every change FUNCTION LOAD REPLACE, name stays stable Calling code stays unchanged with Functions
Replication Effect replication optional, must be enabled Effect replication on by default Functions structurally more consistent
Declarative flags Not available no-writes, allow-stale, and more Functions enforce safeguards technically
Best suited for One-off ad-hoc scripts Long-lived server logic Choose based on the script's expected lifetime

Mironsoft

Cache layer setup and Magento Redis integration

Magento cache that isn't quite working or is misconfigured?

We set up Redis as a cache and session backend for Magento cleanly, tune memory usage and eviction strategies, and make sure full page cache and session storage work together reliably.

Redis Setup

Configure the cache, session, and FPC backend production-ready for Magento.

Memory Tuning

Match memory usage and eviction policies to the shop's actual load.

High Availability Setup

Set up Redis Sentinel or Cluster for resilient Magento environments.

10. Summary

Redis Functions vs. Lua Scripting: The Essentials at a Glance

Core difference

EVAL references anonymous scripts via hashes, Functions load named libraries via FUNCTION LOAD with stable function names.

Maintainability advantage

FUNCTION LOAD REPLACE swaps a library as a whole, without needing any changes to calling code.

Replication advantage

Functions enforce effect replication by default, EVAL requires an explicit redis.replicate_commands() call for that.

Migration strategy

Convert existing scripts into registered functions step by step, test them in parallel, and switch over fully only after verification.

11. FAQ: Redis Functions vs. Lua Scripting: The Essentials at a Glance

1What is the fundamental difference between EVAL and Redis Functions?
EVAL references anonymous Lua scripts via an SHA1 hash computed from the content, while Functions register named libraries with stable function names through FUNCTION LOAD.
2How do you call a registered Function?
Via FCALL with the function name, the number of keys, the keys themselves, and further arguments, or via FCALL_RO for pure read functions without write access.
3Why is update management easier with Functions than with EVAL?
Because FUNCTION LOAD REPLACE fully replaces a library while function names stay stable. With EVAL, the hash changes on every script change, requiring calling code to be updated.
4How does replication behavior differ between EVAL and Functions?
Functions enforce effect replication by default, while EVAL requires a script author to explicitly call redis.replicate_commands() for non-deterministic scripts, which is often forgotten in older scripts.
5What flags can be set when registering a Function?
Among others, no-writes for functions guaranteed to only read, and allow-stale for functions that can run meaningfully even with stale replica data. Redis enforces these flags technically.
6Do loaded Function libraries survive a restart?
Yes, as long as RDB or AOF persistence is active. Libraries are also automatically transferred to replicas without needing to be loaded there again.
7When does classic EVAL still remain the better choice?
For one-off, ad-hoc scripts like manual migrations or temporary debugging scripts, where the overhead of persistent library registration would be disproportionate.
8How does a practical migration of an existing EVAL script proceed?
The Lua code is wrapped in a function definition with a shebang line and redis.register_function(), tested in parallel to EVALSHA, and switched fully to FCALL only after verification.
9How do you remove a library that is no longer needed?
FUNCTION DELETE libraryname removes a single library without affecting others. FUNCTION FLUSH, in contrast, deletes all libraries at once.
10Is Redis Functions available in every Redis version?
No, FUNCTION LOAD is only available from Redis 7.0 onward. In older versions, EVAL remains the only option for server logic for lack of an alternative.