Skip to content
Lesson 27 of 29

Step 1 of 3 · Reading · ~3 min

Server-Side Scripting

Scripting, Replication & Streams

Why servers run scripts, not just commands

Every Redis round-trip costs a network hop. If a piece of logic needs to read a value, compute something, and write a value back — "increment this counter only if it's below a cap," "pop an item and push it to a different list atomically" — doing that from the client means multiple round trips and a race window between them (exactly the problem WATCH exists to police from the outside). EVAL sidesteps both issues by shipping the logic to the data: the script runs entirely inside the server process, so every redis.call() inside it happens without any other client's commands interleaving.

Real Redis embeds a full Lua interpreter for this. We're not going to write a Lua VM — we're going to build the 90%-useful subset: recognize a handful of canonical script shapes and dispatch straight to the command handlers you already wrote in earlier lessons.

The calling convention

EVAL <script> <numkeys> <key1> ... <keyN> <arg1> ... <argM>

numkeys tells the parser where the key list ends and the argument list begins — this is exactly how real Redis distinguishes KEYS[] from ARGV[] inside the script, and it exists so Redis Cluster can statically determine which keys a script touches for routing, without having to actually parse Lua.

KEYS = args[0:numkeys]
ARGV = args[numkeys:]

Pattern-matching instead of parsing

Because the test scripts are fixed strings, you can dispatch with simple prefix/substring checks rather than a real parser:

python

The key insight: each branch calls straight into the same command handlers your GET/SET/INCR code paths already use — EVAL is not a new subsystem, it's a thin adapter in front of your existing dispatch table.

Mapping return values to RESP

Redis's EVAL return-type coercion has fixed rules — the Lua type of the returned value determines the RESP type of the reply:

Script returnsRESP reply
Lua numberInteger (:N\r\n)
Lua stringBulk string ($len\r\n...\r\n)
false / nilNull bulk ($-1\r\n)

So redis.call('SET', ...)'s own +OK reply passes through as a simple string, GET's result passes through as a bulk string (or nil bulk if missing), INCR's result is an integer, and #KEYS — a raw Lua number — is also an integer, not a string.

Atomicity is a side effect of your architecture, not extra work

Because your event loop processes one client command at a time (no other command can start mid-script), correctness here is essentially free — you don't need any locking around the redis.call() invocations inside eval_script. This is the same reason real Redis can offer "scripts are atomic" as a headline feature: it was already single-threaded for command execution before scripting existed.

Up nextREPLICAOF & PSYNC — Master/Replica ReplicationScripting, Replication & Streams

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…