skip to content

No-Rollback Semantics

You will learn why Redis deliberately refuses to roll back a failed command inside EXEC and what that means for your error handling: design batches to be safe under partial success. Interviewers ask 'what happens when the second command fails?' to expose relational assumptions.

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

questions

4

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

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

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

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