skip to content

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%

answer

  1. queue time vs execution time
  2. dirty flag set on bad queue
  3. EXECABORT = nothing ran, safe retry
  4. WRONGTYPE only visible at execution
  5. OOM rejection aborts too — capacity signal

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.

solid answer

~60 s

There are two error windows. **At queue time**, when you send a command after `MULTI`, Redis parses it and looks it up. If the command does not exist, has the wrong arity, or is rejected because the server is over `maxmemory`, the server replies with an error immediately instead of `QUEUED` and flags the transaction as dirty. The subsequent `EXEC` then returns `EXECABORT Transaction discarded because of previous errors.` and **nothing is executed** — this is the only genuinely all-or-nothing path. **At execution time**, everything queued runs. A `WRONGTYPE`, a script error, or any failure that depends on the actual value of a key surfaces as an error element inside the EXEC array reply, with the surrounding commands applied and kept. So `EXECABORT` is good news for state integrity: you know the dataset was untouched and can safely fix and retry. A normal array reply containing an error is the dangerous case — the batch is partially applied and only you can decide what that means. Note this queue-time check is behaviour from Redis 2.6.5 onwards; older servers executed the rest of the batch.

code

text · 12 lines
text
> MULTI
OK
> INCR counter
QUEUED
> INCR
(error) ERR wrong number of arguments for 'incr' command
> SET a 1
QUEUED
> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
> GET counter
(nil)      # nothing ran

go deeper

for a junior

Know that some errors stop the batch before it starts (EXECABORT, nothing applied) and others happen mid-run (partial application).

for a middle

Name the queue-time causes — unknown command, wrong arity, out-of-memory rejection — and explain the dirty flag plus what EXEC returns in each case.

for a senior

Turn it into a client decision tree (EXECABORT / nil / array-with-errors) and note that in production the abort you actually see is memory pressure, not typos.

for a principal

Position it as the only all-or-nothing guarantee Redis offers and reason about what that means for where you can safely place multi-key invariants and how you classify failures in observability.

## Two windows in which a command can fail A Redis transaction has two distinct phases, and an error means different things in each. **Phase 1 — queuing.** After `MULTI`, each command you send is parsed and looked up in the command table, but not executed. The normal reply is `+QUEUED`. Redis can already detect a few problems here without touching data: - the command name does not exist (typo, or a command your server build lacks); - the arity is wrong (too few or too many arguments for that command); - the server is over its `maxmemory` limit and the command is one that may allocate memory, so it is rejected up front. When any of these happens, the server replies with an error instead of `QUEUED` and sets a *dirty* flag on the connection's transaction state. **Phase 2 — execution.** `EXEC` first checks that flag. If it is set, EXEC returns `-EXECABORT Transaction discarded because of previous errors.` and **not one queued command runs**. Otherwise EXEC executes the whole queue and returns an array with one reply per command; any failure discovered now (most commonly `WRONGTYPE`, because the key's actual type is only known at execution) is just an error element in that array, and the neighbouring commands remain applied. ## Why this split exists Queue-time checks are cheap and value-independent: parsing and arity do not require looking at the dataset. Redis can therefore be strict about them for free. Type errors, by contrast, depend on the live value of a key, which may change between queuing and EXEC, so checking them early would be both expensive and unsound. Historically Redis was more lenient: before 2.6.5, a command that failed to queue was simply skipped and the rest of the transaction still ran, which produced silently truncated batches. The modern abort behaviour is strictly safer, and you should not design around the old semantics. ## Reading the outcome as a client This gives you a clean decision tree after EXEC: 1. **`EXECABORT` error reply.** The dataset is untouched. This is a bug in your command construction (or a memory-pressure rejection). Fix the command or shed load and retry the whole batch safely — retrying cannot double-apply anything. 2. **`nil` reply.** Not an error at all: a key you had guarded with `WATCH` changed, so the transaction was cancelled without executing. Also safe to retry. 3. **Array reply.** The batch executed. Scan every element; error elements mean partial application that will not be undone. Many client libraries collapse cases 1 and 3 into a single exception type, which hides exactly the distinction you care about. When correctness matters, verify how your driver distinguishes an `EXECABORT` error, a `nil` EXEC, and an array containing errors. ## The out-of-memory case is the operational one Unknown commands and wrong arity are development-time mistakes you will find in tests. The queue-time rejection that actually fires in production is memory pressure: once the instance is at `maxmemory` and eviction cannot free enough, memory-allocating commands are refused, and every transaction containing one aborts. So a burst of `EXECABORT` in your logs is usually not a code bug — it is a capacity signal. Correlate it with `used_memory`, `maxmemory_policy` and eviction counters from `INFO` before you go looking at your command builder. ## Practical guidance - Build commands from constants or a typed client wrapper so arity and name errors cannot reach production. - Log the EXEC outcome by category (aborted / nil / executed-with-errors), not as one generic failure; the three demand different responses. - Never treat `EXECABORT` as a reason to retry command-by-command outside a transaction — the whole point is that nothing ran, so re-issuing the intact batch is the correct move. - Remember that a `DISCARD` also clears the dirty flag along with the queue, so a connection is reusable after an abort.

  • Your production logs show a spike of EXECABORT. Where do you look first?
    Memory. Once the instance hits `maxmemory` and the eviction policy cannot reclaim enough, commands that may allocate are rejected at queue time, which aborts every transaction containing one. Check `INFO memory` for `used_memory` versus `maxmemory`, the configured policy, and eviction/rejection counters before suspecting a code change.
  • After an EXECABORT, is it safe to retry the same batch?
    Yes, from a data standpoint — nothing was executed, so a retry cannot double-apply anything. But retrying an identical malformed batch will abort again, so the retry only makes sense after fixing the offending command or, in the out-of-memory case, after pressure subsides. Add backoff rather than a tight loop.

saying these in an interview costs you the question

  • Treating EXECABORT and an array reply containing errors as the same failure
  • Thinking a WRONGTYPE is caught at queue time because the key already exists
  • Assuming a mistyped command is simply skipped and the rest of the batch runs (pre-2.6.5 behaviour)
  • Retrying an aborted batch command-by-command outside the transaction 'to be safe'
  • Never inspecting the EXEC reply because the client did not throw

context