Walk through what Redis does with the commands a client sends between MULTI and EXEC, what EXEC returns, and what DISCARD is for.
answer
- MULTI → OK; every command after → +QUEUED
- EXEC = run in order, no interleaving, array of replies
- DISCARD = drop the queue, leave transaction mode
- queue is per connection; MULTI does not nest
- atomic = isolation, not rollback and not durability
basics
~20 sMULTI 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.
solid answer
~50 s`MULTI` returns OK and switches that one connection into transaction mode. Every command sent afterwards is parsed, checked, and appended to a per-connection queue — the server replies `+QUEUED` instead of a real reply, so you cannot see any result yet. `EXEC` executes the queued commands, in the order queued, as a single unit of work on the server. Because Redis runs commands on one thread, nothing from another client can interleave between them, so other clients observe either none of the transaction or all of it. EXEC's reply is an array with one element per queued command, in the same order, each being that command's normal reply — including an error object if a command failed at runtime. `DISCARD` abandons the queue, exits transaction mode and returns OK; nothing runs. Closing the connection before EXEC has the same effect. MULTI cannot be nested, and queuing is per connection, so other clients are unaffected.
code
text · 11 linesMULTI
INCR page:views
HSET user:42 last_seen 1712345678
EXEC
# 1) (integer) 91
# 2) (integer) 0
MULTI
INCR page:views
DISCARD # => OK, the INCR never ran
EXEC # => (error) ERR EXEC without MULTIgo deeper
Recall the three commands and the queue-then-run model: commands after MULTI answer QUEUED, EXEC runs them all in order, DISCARD throws them away.
Add what atomic means concretely — no interleaving from other clients because execution is single-threaded — and that the queue is per connection.
Contrast with pipelining, note that isolation is the real benefit while durability still depends on persistence config, and mention connection hygiene (DISCARD/RESET before returning a pooled connection).
Position MULTI/EXEC against the alternatives — WATCH-based optimistic retries and server-side scripting — and be explicit about which guarantees the primitive does and does not extend to.
## The two phases A Redis transaction has a queueing phase and an execution phase, and almost every misunderstanding comes from conflating them. **Phase 1 — queueing.** `MULTI` returns `OK` and marks this *connection* as being in transaction mode. From that point, each command the client sends is not executed. The server parses it, verifies the command exists and that the argument count is plausible, appends it to a queue held on that connection's state, and replies `+QUEUED`. The client library records that placeholder rather than a real value. **Phase 2 — execution.** `EXEC` takes the queue and runs the commands one after another, in order. The reply is an array with exactly one element per queued command, positionally matched, each containing what that command would normally have returned. ``` MULTI -> OK INCR counter -> QUEUED LPUSH events "a" -> QUEUED EXEC -> 1) (integer) 7 2) (integer) 3 ``` ## What "atomic" means here Redis executes commands on a single thread. EXEC is dispatched as one unit of work, so no other client's command runs between the first and last queued command. Every other connection sees the state either entirely before the transaction or entirely after it — there is no window in which half of it is visible. That is real isolation, and it is the main thing MULTI/EXEC buys you over sending the same commands one at a time. It is a narrower promise than the word "transaction" implies elsewhere. There is no rollback: if one queued command fails at execution time, the ones around it still take effect (a dedicated topic in its own right). And atomic execution says nothing about durability — whether the transaction survives a crash depends entirely on your persistence configuration, not on EXEC. ## DISCARD and the other ways a transaction ends `DISCARD` flushes the queued commands, exits transaction mode and returns OK. Nothing that was queued runs. It is what a client library calls when application code throws while building a transaction, so the connection is not left in transaction mode holding half a batch. Other terminations: - **Client disconnects before EXEC** — the queue dies with the connection state; nothing executes. There is no server-side timeout that fires a partial transaction. - **RESET** (Redis 6.2+) — returns the connection to a clean state: it discards a pending transaction, unwatches keys, exits subscribe mode, and more. Useful for connection-pool hygiene. - **A queue-time error** — if a queued command could not even be accepted (unknown command, wrong arity), the transaction is flagged and EXEC refuses to run anything, returning an EXECABORT error. That error path is worth studying separately. Calling `EXEC` or `DISCARD` outside a transaction is an error ("without MULTI"). `MULTI` inside a transaction is also an error — transactions do not nest, so there is no partial commit or savepoint concept. ## Per-connection state The queue lives on the connection. Two clients can be in the middle of building their own transactions simultaneously and neither can see the other's queued commands; they only contend at EXEC time, and even then the single-threaded execution serialises them. This also means a pooled connection must never be handed back to another caller while still in transaction mode — hence RESET or an explicit DISCARD in the library's cleanup path. A few commands are rejected inside MULTI, notably `SUBSCRIBE`/`UNSUBSCRIBE`/`PSUBSCRIBE` and `WATCH` (WATCH must be issued *before* MULTI), because their semantics do not fit a deferred queue. ## Why this is not the same as pipelining Both reduce round trips: pipelining because the client sends many commands without waiting for each reply, MULTI/EXEC because you only get replies at EXEC. But pipelining gives no isolation — other clients' commands can interleave freely between pipelined commands — while MULTI/EXEC guarantees no interleaving. In practice they are combined: the whole MULTI…EXEC block is sent as one pipelined batch, which is what most client libraries do for you. ## What you cannot do The cost of queueing is that no intermediate results exist before EXEC. You cannot read a value inside the transaction and branch on it, because there is no value to read — only `+QUEUED`. Any logic of the form "read X, then decide what to write" needs either optimistic concurrency with WATCH around the read, or a server-side script that runs the logic where the data is. Finally, if you used WATCH before MULTI and a watched key changed, EXEC returns a null reply instead of the array and nothing executes — the signal that the optimistic check failed and the client should retry.
- Why can't you read a key inside MULTI and use its value to decide the next queued command?Between MULTI and EXEC nothing executes — a queued GET returns only `+QUEUED`, and the real value does not exist until EXEC returns the whole reply array. So there is nothing to branch on at build time. Read-then-decide logic needs WATCH around the read with a retry loop, or a server-side Lua script that performs the read and the branch where the data lives.
- How is MULTI/EXEC different from simply pipelining the same commands?Pipelining is a network optimisation: the client sends many commands without waiting for replies, but other clients' commands may interleave with them on the server. MULTI/EXEC additionally guarantees no interleaving — the queued commands run back to back as one unit. The two combine: clients normally send the whole MULTI…EXEC block in a single pipelined write.
MULTI is filling in an order form line by line — nothing is cooked, you just get a tick per line. EXEC hands the whole form to the kitchen, which does every line back to back before serving anyone else. DISCARD bins the form.
saying these in an interview costs you the question
- Expecting a command's real reply between MULTI and EXEC instead of +QUEUED
- Assuming DISCARD rolls back commands — nothing had executed, so there is nothing to undo
- Thinking a Redis transaction gives rollback on error like a SQL transaction
- Believing MULTI blocks other clients from the moment it is called rather than only during EXEC
- Trying to nest MULTI or expecting savepoints