skip to content

Transactions & Scripting

You will learn every tool Redis offers for atomic multi-command work: MULTI/EXEC queues, WATCH-based optimistic concurrency, Lua scripts, Redis Functions, and how pipelining differs from all of them. Interviewers love this area because Redis 'transactions' defy relational intuition — there is no rollback.

part ofRedisoverview, primer and where to startread it →
on this pageshow

explore

questions

25

A service writes 10,000 keys to Redis by issuing 10,000 separate SET commands in a loop, and the loop takes seconds even though Redis reports each command as sub-millisecond. Explain what Redis pipelining is and how it changes this picture.

level: juniorimportance: must knowfreq 62%

answer

  1. RTT dominates, not Redis CPU
  2. write many, read many, order preserved
  3. client-side only, server unchanged
  4. no atomicity, no isolation
  5. batch hundreds-thousands, not millions

basics

~20 s

The cost is 10,000 network round trips, not Redis work. Pipelining means writing many commands to the socket without waiting for each reply, then reading the replies in order. Same server work, far fewer round trips, often 5-50x faster.

solid answer

~50 s

The bottleneck is the request/response round trip (RTT), not Redis. A naive loop is strictly lockstep: send SET, block, read `+OK`, send the next one. At 0.5 ms RTT, 10,000 commands cost about 5 seconds of pure waiting while Redis sits idle. Pipelining breaks the lockstep: the client writes many commands into the socket back-to-back without waiting, then reads the replies. RESP guarantees replies come back on that connection in the same order the commands were sent, so the client matches reply *i* to command *i* by position. The server does exactly the same per-command work; what disappears is the waiting. In practice you batch in chunks of a few hundred to a few thousand commands, because both sides must buffer outstanding commands and their replies. Pipelining gives ordering on one connection; it does **not** give atomicity or isolation, so other clients' commands may run between yours. For bulk loads, `redis-cli --pipe` does the batching for you.

code

text · 13 lines
text
# lockstep: one round trip per command
redis-benchmark -n 100000 -t set -P 1
#   ~30,000 requests per second on a 0.3 ms RTT link

# pipelined 16 deep
redis-benchmark -n 100000 -t set -P 16
#   ~400,000 requests per second - same server, fewer round trips

# bulk load a file of commands using the built-in pipe mode
cat commands.txt | redis-cli --pipe
# All data transferred. Waiting for the last reply...
# Last reply received from server.
# errors: 0, replies: 100000

go deeper

for a junior

Say clearly that the loop pays one network round trip per command, and that pipelining sends many commands before reading replies. Mention that replies come back in order.

for a middle

Add the mechanism: client-side batching over RESP, server unchanged, ordering per connection, and the explicit caveat that there is no atomicity or isolation.

for a senior

Quantify it (RTT x N), talk about batch sizing against buffers and fairness to other clients, partial application on connection loss, and when a variadic command or Lua is the better tool.

for a principal

Frame it as a latency-budget decision: where the RTT sits in the call graph, how batch depth trades tail latency for throughput, and how the client library and pool topology should encode that rather than each caller hand-rolling batches.

## The problem is round trips, not Redis throughput Every command in a naive client loop is a full request/response cycle: the client writes the command bytes to the TCP socket, then **blocks** until the reply arrives. Elapsed time per command is dominated by the network round-trip time (RTT), not by Redis's execution time. Redis typically executes a `SET` in a few microseconds, but an in-datacenter RTT is 0.1-0.5 ms and a cross-AZ or VPN hop can be several milliseconds. So 10,000 sequential commands cost `10,000 x RTT`, which is seconds, while the server is idle for almost all of it. This ceiling is per connection and is unaffected by how fast Redis is; buying a bigger Redis instance changes nothing. ## What pipelining actually is Pipelining is a **client-side** technique, not a Redis command or mode. The client serializes several commands into the outgoing socket buffer without waiting for a reply between them, then reads the replies afterwards. The server is untouched: it reads whatever bytes have arrived, parses complete commands from its per-client query buffer, executes them one at a time on its single command-processing loop, and appends each reply to that client's output buffer. The correctness rule that makes this safe is RESP's ordering guarantee: on a single connection, replies are emitted in the exact order the commands were received. The client therefore does not need request IDs; it keeps a queue of the commands it sent and pops one entry per reply parsed. That is also why a client must not lose track of how many replies are outstanding: a mismatch desynchronizes the connection permanently. ## What changes and what does not - **Changes:** the number of round trips, hence wall-clock latency of the batch, and syscall overhead (fewer, larger `read`/`write` calls on both sides). - **Does not change:** the work Redis performs per command, memory used by the values, key expiry, or command semantics. Ten pipelined `INCR`s do exactly what ten sequential ones do. - **Does not add:** atomicity, isolation, or a transaction. Between two commands of your pipeline, Redis may execute commands from other clients. If you need the batch to run as one uninterrupted unit, wrap it in `MULTI`/`EXEC` or run a Lua script; pipelining is only about transport. - **Failure mode differs:** if the connection dies mid-pipeline, an arbitrary prefix of the commands may already have been executed. Pipelined batches must therefore be idempotent or recoverable, exactly like any sequence of independent commands. ## Choosing a batch size Bigger is not monotonically better. Every command in flight consumes memory in the server's query buffer, and every reply consumes memory in that client's output buffer until the client reads it. A single enormous pipeline also occupies the server for a long stretch, delaying other clients. Typical guidance is batches of roughly 100-10,000 commands, tuned so a batch is on the order of a few hundred kilobytes to a few megabytes of request plus reply bytes. Read replies as you go rather than deferring a million of them. ## Alternatives and how they relate - **Variadic commands** (`MSET`, `MGET`, `HMSET`, `SADD` with many members, `DEL` with many keys) are a single command, so they are one round trip *and* one unit of execution. Prefer them when they express what you need; pipeline when the commands are heterogeneous. - **Lua scripts** move logic server-side, giving one round trip plus atomic execution, at the cost of blocking the server for the script's duration. - **Connection pooling / concurrency** raises aggregate throughput across many clients but does not fix per-request latency for one logical batch. ## Measuring it `redis-benchmark -P <n>` runs the benchmark with a pipeline depth of `n` and makes the effect obvious: throughput commonly rises by an order of magnitude between `-P 1` and `-P 16` on a real network, then flattens once CPU or bandwidth, rather than RTT, becomes the limit. For one-off bulk loading, `redis-cli --pipe` streams raw RESP from stdin and reports the number of replies and errors at the end. ## What an interviewer is listening for Name RTT as the cost, describe pipelining as batching writes and deferring reads, cite RESP reply ordering as the mechanism that keeps replies matched, and volunteer the two limits: no atomicity, and memory/latency pressure from oversized batches.

  • Why not put a million commands into one pipeline and be done with it?
    Because the outstanding requests sit in the server's per-client query buffer and every reply accumulates in that client's output buffer until you read it, so a huge batch can add hundreds of megabytes of server memory pressure. It also occupies the single command loop for a long stretch, delaying every other client, and it makes crash recovery coarser. Batching a few hundred to a few thousand commands per flush captures nearly all the RTT saving with none of that.
  • How is pipelining different from using a variadic command such as MSET?
    A variadic command is one command: one round trip and one indivisible unit of execution, so nothing from another client can interleave inside it. A pipeline is N independent commands that merely travel together, so other clients can be served between them and each has its own reply. Prefer the variadic form when it expresses the operation; pipeline when the commands differ or target different data types.

Ordering ten coffees one at a time, walking back to your desk after each, versus writing the whole order on one slip and collecting all ten cups together. The barista's work is identical; you just stop pacing back and forth.

saying these in an interview costs you the question

  • Saying pipelining makes the batch atomic or transactional
  • Claiming Redis executes the pipelined commands in parallel
  • Confusing pipelining with MULTI/EXEC, or thinking one implies the other
  • Believing pipelining reduces Redis CPU or memory work per command
  • Assuming unlimited batch size is free because 'it is just network'

context

open as a page

When you run a Lua script on Redis with EVAL, you pass a key count followed by keys and then other arguments, which the script sees as KEYS and ARGV. Why does Redis insist that key names be declared separately instead of just hardcoding them in the script body?

level: juniorimportance: must knowfreq 52%

basics

~20 s

Redis needs to know which keys a script touches before running it, so it can route the command to the right cluster node, enforce ACL key permissions and cross-slot rules. Keys go in KEYS via the numkeys count; everything else goes in ARGV.

open as a page

Walk through what Redis does with the commands a client sends between MULTI and EXEC, what EXEC returns, and what DISCARD is for.

level: juniorimportance: must knowfreq 55%

basics

~20 s

MULTI puts the connection in transaction mode; each following command is only queued and answers +QUEUED, not executed. EXEC runs the whole queue in order with no other client's commands interleaved, returning an array of replies, one per queued command. DISCARD throws the queue away and leaves transaction mode.

open as a page

While one Redis client is sending a batch of commands in a single pipeline, can another client's commands execute in between them? State precisely what guarantees pipelining does and does not give.

level: middleimportance: must knowfreq 52%

basics

~20 s

Yes, other clients can run in between. A pipeline only guarantees that your commands execute in the order you sent them on that connection, and that replies come back in that order. It gives no atomicity, no isolation, and no all-or-nothing behaviour on failure.

open as a page

Redis 7.0 introduced Redis Functions alongside the older EVAL/EVALSHA Lua scripting. What is a Redis Function library, and how does it differ from a script executed with EVAL?

level: middleimportance: must knowfreq 45%

basics

~20 s

A Function library is named Lua code loaded once with FUNCTION LOAD and stored in the dataset itself — persisted to RDB/AOF, replicated, surviving restart. You invoke it by name with FCALL. EVAL scripts are anonymous, cached only, and identified by SHA1.

open as a page

A Lua script sent to Redis with EVAL is described as executing atomically. What exactly does that guarantee, what does it cost the rest of the server, and what can an operator do about a script that will not finish?

level: middleimportance: must knowfreq 54%

basics

~20 s

The script runs to completion with no other client command interleaved, so read-modify-write logic is safe. The cost: it occupies the single command-processing thread, so every other client waits. After busy-reply-threshold Redis replies BUSY and accepts SCRIPT KILL — but only if the script has not written.

open as a page

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%

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.

open as a page

In Redis, you queue several commands between MULTI and EXEC and one of them fails while it is being executed. What happens to the commands before and after it, and why does Redis not undo the ones that already ran?

level: middleimportance: must knowfreq 62%

basics

~20 s

Nothing is undone. Redis runs every queued command; the failing one returns an error in its slot of the EXEC array reply and the others still execute and stay applied. Redis has no rollback: such failures are treated as programming bugs, not runtime conditions.

open as a page

Redis has a WATCH command you can issue before starting a queued command batch. What does it do, what does EXEC return when a watched key was changed, and how do you write the retry loop around it correctly?

level: middleimportance: must knowfreq 55%

basics

~20 s

WATCH marks keys for optimistic locking on your connection. If any client writes a watched key before you call EXEC, EXEC executes nothing and returns a null reply instead of the results array. The client detects the null, re-reads, and retries the whole read-decide-queue-EXEC cycle.

open as a page

Redis will not undo a batch of commands that was only half applied, and a client can lose its connection at any point — including after sending EXEC. How do you design a multi-command write so that retrying it cannot corrupt state?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Assume at-least-once. Prefer idempotent commands (SET, HSET, ZADD) over accumulating ones (INCR, LPUSH, SADD of unique members is fine), order writes so an interrupted batch leaves a benign state, guard non-idempotent effects with a dedupe key set via SET token NX EX, and give orphan-prone keys a TTL.

open as a page

You need a read-modify-write over Redis data that must not lose concurrent updates. Compare doing it with WATCH-based optimistic concurrency against doing it in a single server-side Lua script, and say when you would pick each.

level: seniorimportance: must knowfreq 44%

basics

~20 s

WATCH validates client-side logic and retries on conflict — flexible, but costs multiple round trips per attempt and degrades as contention rises. A Lua script does read, decide and write in one uninterrupted server-side pass — one round trip, no retries — but must be short, same-slot, deployed, and still offers no rollback.

open as a page

You want to run a small piece of named server-side logic on Redis 7 using its Functions feature. What does the Lua library file have to contain, which command installs it, and how do you invoke one of its functions?

level: juniorimportance: should knowfreq 28%

basics

~20 s

The file starts with a shebang naming the engine and library — #!lua name=mylib — and calls redis.register_function('fname', callback). Install it with FUNCTION LOAD, then run it with FCALL fname numkeys key… arg…. The callback receives (keys, args).

open as a page

Why can't application code read a key inside a Redis MULTI block and branch on its value to decide the next queued command, and what should you do when the logic genuinely needs that?

level: middleimportance: should knowfreq 40%

basics

~20 s

Because nothing executes until EXEC: a queued GET replies +QUEUED, and the real value only appears in the EXEC reply array after the whole batch has run. There is no value to branch on at build time. Use WATCH around the read with a retry loop, or a server-side Lua script/Function that reads and decides where the data lives.

open as a page

Redis can reject a batch of queued commands outright so that none of them run, but it can also run a batch in which one command errors. What distinguishes the two cases, and what does an EXECABORT error tell you about the state of the data?

level: middleimportance: should knowfreq 42%

basics

~20 s

Errors Redis can detect while queuing — unknown command, wrong number of arguments, out-of-memory rejection — mark the connection dirty and make EXEC fail with EXECABORT, running nothing. Errors only visible during execution (like WRONGTYPE) let the block run and stay applied.

open as a page

When Redis cancels a transaction because a guarded key changed, what exactly counts as a change? Cover whether writing the same value counts, whether one field of a large hash counts, whether the client's own write counts, and what happens if the key expires on its own.

level: middleimportance: should knowfreq 36%

basics

~20 s

Granularity is the whole key, not a field or element, and the test is 'a write command touched it', not 'the value differs' — rewriting the identical value still cancels. A write on your own connection counts too. In modern Redis a key expiring on its own does not cancel.

open as a page

How do you choose the batch size when pipelining commands to Redis, and which client-side and server-side limits does an oversized batch run into?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Size batches by bytes, not just count: a few hundred to a few thousand commands, roughly under a few megabytes of requests plus replies. Too large hits the server's per-client query-buffer limit, inflates the client output buffer (unlimited for normal clients by default), and monopolises the command loop.

open as a page

What do you gain by wrapping MULTI ... EXEC inside a single Redis pipeline, and what does each of the two mechanisms contribute on its own?

level: seniorimportance: should knowfreq 34%

basics

~20 s

They are orthogonal and compose. MULTI/EXEC contributes indivisible execution: the queued commands run with no other client's command inside them. The pipeline contributes one round trip instead of one per queued command, because each queued command otherwise gets its own +QUEUED reply.

open as a page

Redis has both FCALL and FCALL_RO for invoking server-side functions. What is the difference, what must the function declare for FCALL_RO to be accepted, and why would you use it?

level: seniorimportance: should knowfreq 24%

basics

~20 s

FCALL_RO only runs functions registered with the no-writes flag; any write inside them is rejected. Because they cannot mutate data, they may run on replicas and during states where writes are refused — useful for scaling reads and for read-only ACL grants.

open as a page

How does a Redis 7 server-side function library get onto every node of a deployment — what happens on FUNCTION LOAD with respect to replicas, snapshots and restarts, and which commands would you use to inspect or move the installed libraries?

level: seniorimportance: should knowfreq 26%

basics

~20 s

FUNCTION LOAD on a primary is replicated to its replicas and written to the AOF, and libraries are serialized into RDB, so they survive restart and failover. In Cluster you must load on every shard's primary. Inspect with FUNCTION LIST/STATS; move with FUNCTION DUMP/RESTORE.

open as a page

Inside a Redis Lua script you can invoke commands with either redis.call or redis.pcall. What is the difference in how they handle a command that fails, and which should you reach for?

level: seniorimportance: should knowfreq 30%

basics

~20 s

redis.call raises a Lua error on command failure, aborting the script and returning the error to the client. redis.pcall returns the error as a Lua table instead, so the script keeps running and must inspect it. Default to redis.call; use pcall only when you will handle the failure.

open as a page

Redis Lua scripts historically had strict determinism rules — no random values, no reliance on the clock, sorted output for unordered commands. Why did those rules exist, and what changed once Redis started replicating script effects instead of the script itself?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Originally Redis shipped the script itself to replicas and the AOF, so both sides had to produce identical results — any randomness or clock use would diverge them. Modern Redis replicates the script's resulting write commands instead, so nondeterminism is safe, though determinism still aids reasoning.

open as a page

When Redis documentation calls a MULTI/EXEC transaction atomic, exactly which guarantees does that cover — with respect to other clients, to a crash, and to replicas?

level: seniorimportance: should knowfreq 38%

basics

~20 s

It covers isolation only: EXEC runs the queued commands back to back on the single execution thread, so no other client sees a half-applied state. It does not cover durability — survival of a crash depends on your AOF/RDB settings — and replication is asynchronous, so a committed EXEC can be lost in a failover.

open as a page

Your team wants Redis to hold an invariant that spans several keys — for example an index that must never point at a missing object, or a balance that must never go negative. Redis offers no rollback and no constraints. How do you decide whether to enforce that invariant in Redis, and what do you put in place if you do?

level: principalimportance: should knowfreq 30%

basics

~20 s

Decide by blast radius: enforce it in Redis only when one atomic unit (a single command or one Lua script over co-located keys) can express the whole check-and-write. Otherwise keep the authoritative invariant in a store with constraints and treat Redis as a derived view with TTLs and reconciliation.

open as a page

An optimistic check-and-set loop against Redis works fine in staging but in production one popular key sees so much concurrent traffic that most attempts are cancelled and the service latency rises. How do you diagnose and fix this?

level: principalimportance: should knowfreq 28%

basics

~20 s

Measure attempts per success and who writes the key. Then remove the contention rather than tune the loop: use a single atomic command if one exists, move the logic into one Lua script, shard the key so writers no longer collide, or batch updates. Keep bounded, jittered retries as a backstop only.

open as a page

Your services run a dozen pieces of Lua logic on Redis via EVALSHA with a NOSCRIPT fallback, and the servers were just upgraded to Redis 7. How would you decide whether to migrate that logic to Redis Functions, and how would you carry out the migration?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Migrate when the logic is long-lived, shared across services, and its NOSCRIPT/versioning handling costs you — Functions give named, deployed, replicated code. Keep EVAL for ad-hoc or client-generated scripts. Migrate side-by-side: load a new library, dual-run, cut traffic over, delete the old path.

open as a page