skip to content

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%

answer

  1. atomic = isolated, not all-or-nothing
  2. WRONGTYPE inside EXEC: rest still run
  3. errors are elements of the EXEC array
  4. no undo log = no speed penalty on writes
  5. only AOF tail crash gives a partial block

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.

solid answer

~60 s

A Redis transaction is atomic only in the sense of **isolation**: the queued commands run back to back on the server with no other client's command interleaved. It is not all-or-nothing. If a command fails at execution time — classically `WRONGTYPE`, e.g. `LPUSH` against a key holding a string — that error is returned as one element of the array EXEC replies with, and every other queued command still runs and its effects stand. Redis never rolls back. The stated rationale is that a command inside EXEC can realistically fail only from a programming error (wrong type, wrong arity) that should be caught in development, so paying for undo logging on the hot write path would cost speed and complexity for a failure class that should not reach production. Redis also has no constraints, unique indexes or foreign keys — the usual reasons a SQL statement aborts mid-transaction simply do not exist. Practically: inspect every element of the EXEC reply (many clients just hand you the list), and design batches so that a partially applied batch is still a recoverable state.

code

text · 16 lines
text
> SET user:1 "a string"
OK
> MULTI
OK
> INCR counter
QUEUED
> LPUSH user:1 x
QUEUED
> SADD tags a
QUEUED
> EXEC
1) (integer) 1
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 1
> GET counter
"1"        # applied and kept

go deeper

for a junior

Recall the fact cleanly: Redis has no rollback; a bad command inside EXEC errors on its own line while the others still apply.

for a middle

Explain the mechanism — queue then execute, one reply per command, errors as array elements — and give the WRONGTYPE example plus the rationale (failures are programming errors).

for a senior

Add the operational consequence: check the whole reply vector, know your driver's error behaviour, order commands so partial application is benign, and mention the AOF-tail corner case.

for a principal

Frame it as a deliberate design tradeoff — no constraints means no legitimate mid-transaction abort, so undo machinery would tax every write — and derive the guidance that invariants spanning keys must be enforced by design or reconciled, not by the store.

## What EXEC actually promises After `MULTI`, the commands you send are not executed — the server queues them on your connection and replies `QUEUED` to each. `EXEC` then executes the whole queue as one uninterrupted unit: no other client's command runs in the middle, and the client receives an array reply with exactly one entry per queued command, in order. That is the whole guarantee. "Atomic" here means *serialized and uninterrupted*, not *all-or-nothing*. Many candidates carry the SQL meaning over and assume a failure aborts and reverts the batch. It does not. ## Execution-time failure The canonical case is a type error. Suppose `user:1` holds a string and the batch contains `INCR counter`, `LPUSH user:1 x`, `SADD tags a`. All three run. The reply array is `[1, (error) WRONGTYPE ..., 1]`. The `INCR` and the `SADD` are permanently applied; there is no compensating action taken by the server. The same is true for a command that errors for any other runtime reason. A subtlety that bites teams: several client libraries return the array as-is, so an error buried at index 5 is silently ignored unless you look. Others raise on the first error element, which can make you believe the batch aborted when in fact it fully executed. Know which behaviour your driver has. ## Why the design is this way The Redis authors give two reasons. First, commands inside a transaction can fail only from *programming errors*: calling a command against the wrong data type, or with wrong arity — mistakes that are visible in development and testing rather than emergent under production load. Redis has no unique constraints, no foreign keys, no check constraints, no deadlock aborts — the classic sources of mid-transaction failure in a relational engine are absent by construction. Second, rollback needs machinery: an undo log or versioned copies of every value touched, plus the branch on the write path to maintain it. That is a permanent cost on every write in a system whose selling point is microsecond-scale operations, bought to protect against a bug you should have caught. So the design pushes correctness up into the application: pick the right commands, test the batch, and make the batch tolerant of partial application. ## The other failure class Distinguish this from failures detected *while queuing* — an unknown command name or wrong arity is rejected immediately with an error at queue time, the transaction is flagged, and the later `EXEC` fails with `EXECABORT` having executed nothing. So Redis does have an all-or-nothing case, but only for errors it can see before execution begins. Once EXEC starts, everything runs. ## Durability and replication corners A transaction is propagated to replicas and to the AOF wrapped in `MULTI`/`EXEC`, so a replica applies the block as a unit rather than half of it. The one place a partial transaction can survive is the append-only file: if the process is killed while writing the AOF, the tail can contain half a transaction. Redis detects that at startup and refuses to load; `redis-check-aof --fix` truncates the incomplete tail. That is the exception that proves the rule — the recovery is truncation, not rollback. Also note that wrapping the same logic in a Lua script or a Redis Function does not change anything here. A script runs atomically in the isolation sense, but if it errors after issuing writes, those writes stand and the client gets an error. ## What to do about it - Treat the EXEC reply as a per-command result vector and check it. - Assume any batch may be observed half-applied by a crashed client, and choose command order so the intermediate state is benign (write the payload before the pointer to it, not after). - Prefer idempotent commands (`SET`, `HSET`, `ZADD`) over accumulating ones (`INCR`, `LPUSH`) where a retry is possible. - Enforce invariants with a single command where one exists (`SET key val NX`, `HSETNX`, `INCRBY`) instead of a multi-command batch you wish were transactional.

  • If a command fails at execution time, does the client get told the transaction failed?
    Not as a whole. EXEC still returns a normal array reply; the failure shows up only as an error element inside that array, at the position of the offending command. Whether you notice depends on your client library — some surface the raw list, some throw on the first error element. You must inspect every entry yourself if correctness depends on it.
  • Does moving the same commands into a Lua script give you rollback?
    No. A script executes atomically in the isolation sense — nothing else runs while it does — but if it raises an error partway through, the writes it already performed remain in the dataset and the client receives the error. Scripts buy you read-modify-write logic without round trips, not transactional undo.
  • Is there any situation where Redis ends up with a half-executed transaction on disk?
    Yes: if the server is killed while appending an EXEC block to the AOF, the file tail can hold a partial transaction. On restart Redis refuses to load the corrupted AOF and `redis-check-aof --fix` truncates the incomplete tail. Replication does not have this problem because the block is sent to replicas wrapped in MULTI/EXEC and applied as a unit.

EXEC is a conveyor belt that runs to the end, not a contract you can void: a jammed item is marked defective and the rest keep moving down the line.

saying these in an interview costs you the question

  • Saying a failed command inside EXEC rolls back the whole transaction like in SQL
  • Assuming the client raising an exception means nothing was applied
  • Believing Redis keeps an undo log or supports SAVEPOINT/ROLLBACK
  • Claiming Lua scripts add rollback semantics on top of Redis
  • Confusing isolation (no interleaving) with atomicity in the all-or-nothing sense

context