R3.04.3 · dive 3
EVALSHA and NOSCRIPT
Load the script once, call it by SHA. A flushed cache returns NOSCRIPT — so the client falls back to EVAL with the body, and re-caches.
The transition runs by its SHA, not its body. SCRIPT LOAD caches the
body and returns a SHA; the client calls EVALSHA sha with no body on the wire. A
restart or SCRIPT FLUSH clears the cache, and the next EVALSHA
returns NOSCRIPT. Toggle the cache below and watch which command actually runs and how
many bytes cross the wire.
Issue EVALSHA · cached hit, or NOSCRIPT then EVAL
Load once, call by SHA
A Lua script can be sent on every call with EVAL script numkeys …, but that puts the
whole body on the wire each time — for a fourteen-key lifecycle script, kilobytes of Lua per transition.
Redis offers a cheaper path. SCRIPT LOAD script caches the body on the server once and
returns its SHA-1 digest. From then on the client calls EVALSHA sha numkeys … —
the forty-character SHA stands in for the body, and Redis runs the cached script. The transition is identical;
only the bytes on the wire change.
# load the body once — Redis returns its SHA-1 digest SCRIPT LOAD "<moveToFinished body>" → "a1b2c3…" (40-char SHA) # from now on, call by SHA — no body on the wire EVALSHA a1b2c3… 14 wait active prioritized ... marker jobId ts ...
EchoMQ takes this path. Its scripts are loaded and cached, and every named-script call runs by
EVALSHA. The fourteen-key moveToFinished reaches Redis as a
SHA plus its keys and args, not as a body — so a high-throughput worker pool is not re-sending the
lifecycle script on every finish.
SCRIPT LOAD caches the body once and returns a SHA; the client then calls
EVALSHA sha with the digest in place of the body.
The NOSCRIPT fallback
The server's script cache is not permanent. A restart clears it; SCRIPT FLUSH clears
it. After that, an EVALSHA for a SHA the server no longer holds returns a
NOSCRIPT error — the SHA is unknown, the body is needed. A robust client handles
this without giving up the transition: on NOSCRIPT, send the full body with
EVAL, which both runs the script and re-caches it under the same SHA. The next
EVALSHA hits the cache again.
# EchoMQ.Scripts.execute_raw/4 — EVALSHA, fall back to EVAL on NOSCRIPT (real, Elixir) case RedisConnection.command(conn, ["EVALSHA", sha, num_keys | keys ++ encoded_args]) do {:ok, result} -> {:ok, decode_result(result)} {:error, %Redix.Error{message: "NOSCRIPT" <> _}} -> # cache was flushed (restart / SCRIPT FLUSH) → load the body and EVAL RedisConnection.command(conn, ["EVAL", script, num_keys | keys ++ encoded_args]) end
EchoMQ.Scripts.execute_raw/4 is exactly this fallback. It tries
EVALSHA first; on the error it matches
{:error, %Redix.Error{message: "NOSCRIPT" <> _}} and re-runs with
EVAL and the full body. The transition succeeds either way — the only cost of a
flushed cache is one fallback round trip that re-caches the script.
A flushed cache turns one EVALSHA into a NOSCRIPT;
the client falls back to EVAL with the body, which runs and re-caches — the
transition never fails.
The wire cost
The contrast is concrete. EVAL every time sends the whole body on every call: N
transitions cost N script bodies on the wire. EVALSHA after one
SCRIPT LOAD sends the body once and a SHA thereafter: N transitions cost one body plus
N short SHAs. For a fourteen-key lifecycle script under a busy worker pool, that is the difference between
re-sending the protocol on every finish and sending a digest. Pick the strategy below and read the wire cost for
a run of transitions.
Bytes on the wire for ten transitions · EVAL every time vs EVALSHA after one load
Run the read-modify-write as one server-side script, and send it cheaply: load the body once, call it by SHA, fall back to the body only when the cache is gone.
execute_raw/4 tries EVALSHA (the cached SHA), and on
NOSCRIPT falls back to EVAL (the full body), which re-caches
— cheap on the wire and resilient to a flushed cache.
EVALSHA sends the body once and a digest thereafter; the fallback re-sends
the body only when the cache is actually gone — once per flush, not once per call.
In EchoMQ — EVALSHA with a NOSCRIPT fallback
EchoMQ's EchoMQ.Scripts moduledoc states that scripts are "loaded and cached for
efficient execution using Redis EVALSHA," and that it uses EVALSHA for efficiency,
"falling back to EVAL if the script" is not cached. That fallback is
EchoMQ.Scripts.execute_raw/4: an EVALSHA attempt, a match on
the NOSCRIPT Redix.Error, and an
EVAL re-run with the body. The fourteen-key moveToFinished
transition runs cheaply by SHA, and a restart that empties the cache costs only one fallback round trip per
script.
R3.04.3 closes the module: a job's states are Redis locations, the transition between them is one
EVALSHA over fourteen keys, and that EVALSHA reaches the server
by SHA with a NOSCRIPT fallback. Return to the
Atomic state machine hub or the
Reliable Queues chapter.
References
The cached-script call, the load that caches it, the full-body fallback, and the scripting model behind the SHA cache — with the queue protocol EchoMQ loads and runs.
Sources
- Redis — EVALSHA — run a cached script by its SHA; the
NOSCRIPTerror when the SHA is unknown. - Redis — SCRIPT LOAD — cache the body once and get back the SHA the client calls by.
- Redis — EVAL / scripting — the full-body call the client falls back to on
NOSCRIPT. - Redis — Scripting with Lua — the script-caching model behind
EVALSHAand theNOSCRIPTfallback. - BullMQ — the queue protocol — where "the Lua scripts are the protocol" EchoMQ loads and runs by SHA.
Related in this course
- R3.04 · Atomic state machine — the module: the transition as one EVALSHA.
- R3.04.2 · Read-decide-write in one EVALSHA — the prior dive: the fourteen-key transition this runs by SHA.
- R3.04.1 · States as locations — the locations the transition travels between.
- R3.03.3 · Atomic vs non-atomic — the EVALSHA the recovery sweep also runs.
- R2.01 · Atomic updates — the one-script read-modify-write pattern.
- E2 · The engine — the dedicated EchoMQ course: the script dispatch and SHA cache in full.