jonnifyredis

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

EVALSHA <sha> 40-byte SHA on the wire cache HIT the script runs server cache: holds <sha> EVALSHA hits the cache · the script runs · no body on the wire

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

10 transitions ≈ 2.4 KB on the wire EVALSHA: one 2 KB body + nine 40-byte SHAs
the pattern

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.

in EchoMQ

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.

Door · not depth The full script-load and SHA-cache management across the worker pool — when scripts are loaded, how the SHAs are held — is the dedicated EchoMQ course. From here, this dive opens onto E2 · the engine — the script dispatch and the SHA cache in full. This dive teaches how the script reaches the server; that course teaches the engine that loads and runs it.

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

Related in this course