skip to content

Redis lets you run a Lua script either by sending its full source with EVAL or by sending a SHA1 digest with EVALSHA. Why does EVALSHA exist, what error must a client be ready for, and how do SCRIPT LOAD and SCRIPT EXISTS fit in?

level: middleimportance: must knowfreq 48%

answer

  1. SHA1 of the exact bytes = cache key
  2. Cache is memory-only: no RDB/AOF, dies on restart/failover
  3. NOSCRIPT → SCRIPT LOAD or EVAL, then retry
  4. SCRIPT LOAD caches without running; SCRIPT EXISTS checks
  5. Cache never evicts → dynamic scripts leak

basics

~20 s

EVALSHA sends only the script's SHA1 instead of its body, saving bandwidth. Redis caches every script it has seen, but that cache is ephemeral, so EVALSHA can fail with NOSCRIPT — the client must then SCRIPT LOAD or fall back to EVAL and retry.

solid answer

~50 s

Every script Redis executes is stored in a **script cache** keyed by the SHA1 of its body. `EVALSHA <sha1> numkeys …` runs the cached copy, so a hot script costs 40 hex characters per call instead of kilobytes of Lua — meaningful on high-throughput paths. The cache is **not** part of the dataset: it is never persisted to RDB or AOF, and it is emptied by `SCRIPT FLUSH`, by a restart, and it is cold on a freshly promoted replica after failover. So `EVALSHA` can return `NOSCRIPT`. Every client must handle it: catch the error, `SCRIPT LOAD <body>` (which returns the same SHA1 and only populates the cache) or simply call `EVAL` with the body, then retry. Most client libraries do this for you. `SCRIPT EXISTS <sha1> …` returns 1/0 per digest, useful to warm or verify a cache. The SHA1 is a plain content digest — you can compute it client-side without talking to Redis.

code

text · 17 lines
text
> SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
"9c8b3e...4f1a"

> SCRIPT EXISTS 9c8b3e...4f1a
1) (integer) 1

> EVALSHA 9c8b3e...4f1a 1 counter:a
(integer) 1

> SCRIPT FLUSH
OK

> EVALSHA 9c8b3e...4f1a 1 counter:a
(error) NOSCRIPT No matching script. Please use EVAL.

> EVAL "return redis.call('INCR', KEYS[1])" 1 counter:a
(integer) 2      # and the script is cached again

go deeper

for a junior

Know that EVALSHA sends a hash instead of the script, that Redis caches scripts, and that NOSCRIPT means you must send the body again.

for a middle

Explain that the cache is memory-only and dies on restart, flush or failover, and describe the SCRIPT LOAD / EXISTS / FLUSH surface plus the retry pattern.

for a senior

Diagnose NOSCRIPT storms (restart, failover, unwarmed node after a reshard, dynamically generated bodies), note the per-node cache in Cluster, and the non-evicting cache as a leak.

for a principal

Argue when the NOSCRIPT contract is worth eliminating entirely by moving stable logic to persisted Function libraries, and weigh the client-support and deployment costs of doing so.

## The bandwidth problem EVALSHA solves `EVAL` carries the entire Lua source in every request. A 2 KB script called 50,000 times a second is 100 MB/s of duplicated text, plus the parsing cost. Redis therefore keeps a **script cache**: whenever it executes a script it stores the body under the SHA1 hex digest of the exact bytes. `EVALSHA <sha1> numkeys key… arg…` then executes the cached copy, and the request shrinks to the digest. Because the key is a content digest, two clients that send byte-identical scripts share the cache entry, and you can compute the digest locally (any SHA1 implementation over the script text) instead of asking the server for it. A single trailing whitespace change makes a different script — this is a real source of "why is my cache full of near-duplicates". ## The cache is ephemeral — this is the whole lesson The script cache lives only in the server's memory and is deliberately not treated as data: - it is **not** written to RDB snapshots; - it is **not** stored in the AOF as a cache (only the script's *effects* are propagated); - `SCRIPT FLUSH [ASYNC|SYNC]` empties it; - a restart starts with an empty cache; - a replica promoted by failover generally has an empty (or differently populated) cache. Hence `EVALSHA` may fail with: ``` (error) NOSCRIPT No matching script. Please use EVAL. ``` This is not an exceptional condition to log and give up on — it is a normal part of the protocol. ## The required client pattern ``` try EVALSHA(sha, keys, args) catch NOSCRIPT: EVAL(body, keys, args) # or SCRIPT LOAD then retry EVALSHA ``` Two important consequences: 1. **The client must still hold the source.** EVALSHA never lets you stop shipping the script text with your application; it only lets you stop sending it on the wire most of the time. 2. **The fallback path is rarely exercised**, so it is rarely tested — and it fires precisely during incidents (after a restart, after a failover, after an operator ran `SCRIPT FLUSH`). Test it deliberately. Most mature client libraries (redis-py, Lettuce, ioredis, go-redis) encapsulate this in a "script object" that computes the digest, tries EVALSHA, and transparently reloads on NOSCRIPT. ## SCRIPT LOAD and SCRIPT EXISTS `SCRIPT LOAD <body>` compiles and caches the script **without executing it**, returning its SHA1. Use it to warm the cache at connection or deployment time. Note that in a Cluster the cache is per node, so warming means loading against every node you may talk to — and even then a restart re-empties it, so warming complements the NOSCRIPT handler, it does not replace it. `SCRIPT EXISTS <sha1> [<sha1> …]` returns an array of 1/0 telling you which digests are cached. It is handy in health checks and in deploy scripts, and for diagnosing a node that keeps returning NOSCRIPT. `SCRIPT FLUSH` empties the cache; it is the command that makes NOSCRIPT happen on a live system, so treat it as disruptive on a busy server. ## Semantics are identical `EVAL` and `EVALSHA` differ only in how the body reaches the server. Both execute atomically, both block the server for the duration, both require keys to be declared, and both propagate to replicas and the AOF the same way (effects replication). There is no execution-speed advantage to `EVALSHA` beyond skipping transmission and a re-parse. Since Redis 7.0 there are also `EVAL_RO` and `EVALSHA_RO`, which refuse write commands and can therefore run on replicas — the script equivalent of the read-only invocation. ## Diagnosing NOSCRIPT storms If a service suddenly floods with NOSCRIPT, the cause is almost always one of: a node restarted or failed over; someone ran `SCRIPT FLUSH`; a client is connecting to a node it never warmed (new node after a reshard, or connection pool spreading across a cluster); or the script text is being generated dynamically so each call has a different digest and the cache grows without ever hitting. That last case also leaks memory in the script cache, since entries are never evicted — which is one motivation for moving stable logic to Redis Functions, whose libraries are named and persisted.

  • Should your application ever stop shipping the script source because it uses EVALSHA?
    No. The cache can vanish at any moment — restart, failover, SCRIPT FLUSH — and the only recovery is re-sending the body. The source must stay in the application (or you must move to Redis Functions, where the library is persisted server-side and called by name with FCALL, which genuinely removes the NOSCRIPT path).
  • A service generates its Lua on the fly, substituting values into the script text before hashing it. What goes wrong?
    Every distinct body has a distinct SHA1, so the cache hit rate collapses and each call effectively behaves like EVAL. Worse, the script cache is never evicted, so the server accumulates one entry per generated variant and slowly leaks memory until SCRIPT FLUSH or a restart. The fix is a single static script whose variable parts arrive through KEYS and ARGV.
  • In Redis Cluster, does SCRIPT LOAD on one node warm the whole cluster?
    No. The script cache is per node, so a digest cached on one shard is unknown on another, and a client whose pool spans nodes will see NOSCRIPT from the nodes it never warmed. Either load against every node you may address, or rely on the client's NOSCRIPT fallback, which is required anyway because restarts and failovers empty individual caches.

saying these in an interview costs you the question

  • Believing the script cache is persisted or replicated
  • Treating NOSCRIPT as an exceptional error rather than a normal path to handle
  • Dropping the script source from the app because EVALSHA has the hash
  • Assuming EVALSHA executes faster than EVAL beyond the transmission and parse saving
  • Generating script text dynamically and expecting cache hits

context