capabilities and limits in detail
Redis transactions queue commands with MULTI and execute them atomically with EXEC, without any other client interleaving its own commands. But anyone who assumes MULTI/EXEC behaves like a SQL transaction with rollback on runtime errors is working from a false assumption that can lead to inconsistent data in production.
Table of Contents
- 1. What Redis transactions really are
- 2. Command queueing: how MULTI collects commands
- 3. EXEC and DISCARD in practice
- 4. WATCH and optimistic locking
- 5. Practical example: transferring a balance safely
- 6. Why there is no rollback on runtime errors
- 7. Two error types: queueing errors vs. runtime errors
- 8. When Lua scripts are the better choice
- 9. MULTI/EXEC compared to other mechanisms
- 10. Summary
- 11. FAQ
1. What Redis transactions really are
A Redis transaction is, at its core, something different from what the term suggests coming from the relational database world. MULTI starts a queueing mode in which all subsequent commands are not executed immediately but placed into a queue. Only EXEC then runs all queued commands one after another, without interruption from other clients. This isolation is the central guarantee of Redis transactions: no other connected client can interleave its own commands between two commands of a transaction.
This guarantee works because Redis operates single threaded and fundamentally processes commands sequentially. A Redis transaction exploits this property to run several commands as a contiguous block, without needing a locking mechanism in the classic sense. This fundamentally distinguishes Redis transactions from SQL transactions, which are based on undo logs, rollback segments and multiple concurrently running threads with lock management.
Important for understanding: Redis transactions guarantee isolation and partial atomicity, but not full ACID semantics. There is no durability guarantee in the sense of a write-ahead log written only after the transaction, and above all there is no automatic rollback when a command inside the transaction fails at runtime. This property is the most frequent source of confusion for developers coming from the relational world.
2. Command queueing: how MULTI collects commands
As soon as a client sends MULTI, the connection switches into a special state. Every command sent afterward is not executed by Redis, only syntactically validated and placed into an internal queue specific to that connection. Redis responds to every queued command immediately with QUEUED, which is already an important first signal for the client: the command was syntactically accepted and is waiting for execution.
This queueing is purely local to the respective connection. Other clients can continue working with Redis normally during this time, their commands are not blocked. Only when EXEC is sent does Redis leave the queueing mode, briefly locks out execution for other clients internally and processes the entire queue sequentially before the next client gets its turn. This short but guaranteed interruption-free window is the actual core benefit of Redis transactions.
# Start a transaction: commands are queued, not executed yet
redis-cli> MULTI
OK
redis-cli> SET account:1001:balance 500
QUEUED
redis-cli> INCRBY account:1001:balance -100
QUEUED
redis-cli> INCRBY account:1002:balance 100
QUEUED
# Nothing has happened yet, all three commands wait in the queue
redis-cli> GET account:1001:balance
(nil)
# EXEC runs the entire queue atomically, without interruption
redis-cli> EXEC
1) OK
2) (integer) 400
3) (integer) 100
3. EXEC and DISCARD in practice
EXEC executes all queued commands in the order they were enqueued and returns an array with the individual responses, in exactly the same order. During execution no other client can interleave commands between the individual steps, because Redis does not hand off the event loop to other connections for the duration of the transaction. This is the decisive difference from a simple sequence of individual commands without MULTI, where other clients could theoretically interleave.
DISCARD aborts a running transaction before EXEC and clears the queue without a single command being executed. This is useful when an application detects during queueing that a precondition is not met and wants to cleanly discard the entire transaction instead of executing it. After DISCARD the connection fully leaves transaction mode and works normally again.
# DISCARD aborts the queue entirely, nothing gets executed
redis-cli> MULTI
OK
redis-cli> DEL session:temp
QUEUED
redis-cli> DISCARD
OK
# The connection is back to normal command mode
redis-cli> EXEC
(error) ERR EXEC without MULTI
4. WATCH and optimistic locking
WATCH is the mechanism that makes Redis transactions genuinely useful for concurrent use cases. With WATCH key, a client marks one or more keys for observation before starting MULTI. If the value of a watched key is changed by another client between WATCH and EXEC, EXEC automatically aborts the entire transaction and returns nil instead. Not a single queued command is then executed.
This behavior implements classic optimistic locking, also known as the compare-and-swap pattern. Instead of acquiring a lock that blocks other clients, the application optimistically assumes no conflicts will occur and only checks at EXEC time whether that assumption still holds. This is especially efficient at low conflict frequency, because no client is ever truly blocked, at worst it has to retry the transaction.
The typical flow: WATCH on the relevant key, then read the current value and prepare the new calculation, then start MULTI, queue the computed commands and call EXEC. If EXEC fails because the key changed in the meantime, the application repeats the entire cycle in a retry loop. UNWATCH removes all observations manually, but is also called automatically after every EXEC or DISCARD anyway.
# Optimistic locking: watch the key, then queue commands based on its value
redis-cli> WATCH inventory:sku-4711:stock
OK
redis-cli> GET inventory:sku-4711:stock
"12"
# Application logic decides: deduct 3 units if stock is sufficient
redis-cli> MULTI
OK
redis-cli> DECRBY inventory:sku-4711:stock 3
QUEUED
# If another client modified inventory:sku-4711:stock in between,
# EXEC aborts and returns nil, the application must retry the cycle
redis-cli> EXEC
(nil)
# Manually clear all watched keys without running a transaction
redis-cli> UNWATCH
OK
5. Practical example: transferring a balance safely
A classic example for Redis transactions is transferring an amount between two balances stored as plain keys. Without a transaction, a crash or network error between deducting from the source account and crediting the target account could cause money to disappear. With MULTI/EXEC, both operations are executed as a contiguous block that no other client can interrupt.
Combined with WATCH on the source account, the application additionally ensures that the balance was not changed between the check and the actual transaction by a concurrent request, such as a simultaneous withdrawal. Only when WATCH reports no change and EXEC completes successfully is the transfer considered complete. This combination of isolation through MULTI/EXEC and conflict detection through WATCH is the standard pattern for concurrency-safe Redis transactions.
6. Why there is no rollback on runtime errors
The most important difference between Redis transactions and SQL transactions concerns behavior on errors that only occur during the execution of a command. If a command inside a transaction is applied to the wrong data type, for example INCR on a string value that is not a number, exactly that one command fails. All other commands of the transaction are still executed, there is no automatic rollback of the steps already successfully executed.
This behavior is a deliberate design decision by the Redis developers, not a limitation due to lack of time. The reasoning: runtime errors in Redis almost always stem from programming mistakes, such as wrong data types, and not from transient system states that a rollback could meaningfully undo. A rollback mechanism would introduce significant complexity and performance cost, for a problem that should already be caught by tests during development, before it reaches production.
# Runtime error inside a transaction does NOT roll back other commands
redis-cli> SET counter "not-a-number"
OK
redis-cli> MULTI
OK
redis-cli> SET other:key "value-one"
QUEUED
redis-cli> INCR counter
QUEUED
redis-cli> SET another:key "value-two"
QUEUED
redis-cli> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
# other:key and another:key were both set, no rollback happened
redis-cli> GET other:key
"value-one"
redis-cli> GET another:key
"value-two"
7. Two error types: queueing errors vs. runtime errors
Redis strictly distinguishes between two error categories within a transaction. Queueing errors occur when a command is already recognized as invalid during enqueueing, for example an unknown command name or a wrong number of arguments. Redis marks these errors internally, and when EXEC is called afterward, Redis refuses the entire execution with an EXECABORT error. In that case, not a single command of the transaction is actually executed, which is sometimes mistakenly interpreted as general rollback behavior.
Runtime errors, on the other hand, as shown in the previous section, are only detected during actual execution by EXEC, because Redis only checks the data type of a key at runtime, not during queueing. These errors do not abort the rest of the transaction. For developers this means: anyone relying on automatic consistency must actively check whether individual commands in the EXEC response contain an error, and implement compensating logic themselves if needed.
8. When Lua scripts are the better choice
For use cases that require conditional logic between multiple commands, for example "read a value and execute different write operations depending on the result", MULTI/EXEC hits a structural limit: within a transaction, read values cannot be evaluated to decide which command gets queued next, because all commands must be fixed before execution. This is exactly where server side Lua scripts with EVAL are the more suitable solution, because they can execute real conditional logic directly inside the Redis process.
A Lua script is also more atomic than a MULTI/EXEC transaction in terms of error handling, because a runtime error in the script immediately stops its further execution, instead of letting the remaining commands run as MULTI/EXEC does. For simple, unconditional command sequences without intermediate evaluation, MULTI/EXEC still remains the simpler and more readable choice, especially combined with WATCH for optimistic locking.
9. MULTI/EXEC compared to other mechanisms
The choice between MULTI/EXEC, WATCH based optimistic locking and Lua scripts depends on the concrete use case. The following table contrasts the central properties.
| Property | MULTI/EXEC | MULTI/EXEC with WATCH | Lua script (EVAL) |
|---|---|---|---|
| Isolation from other clients | Yes | Yes | Yes |
| Conflict detection under concurrency | No | Yes, via retry loop | Not needed, runs synchronously |
| Conditional logic between commands | Not possible | Not within the transaction | Fully supported |
| Rollback on runtime error | No rollback | No rollback | Script stops on error |
| Network round trips | Multiple (per command) | Multiple plus WATCH/GET | A single round trip |
For simple, unconditional command chains, MULTI/EXEC is sufficient and easy to read. As soon as concurrency becomes relevant, WATCH adds the necessary conflict detection. As soon as conditional logic comes into play or the number of network round trips becomes critical, a Lua script is the more robust and usually also more performant alternative.
10. Summary
Redis transactions with MULTI and EXEC guarantee isolation from other clients by executing all queued commands sequentially without interruption. WATCH adds optimistic locking and allows transactions to be safely aborted and retried on concurrent changes. The decisive limit: runtime errors within a transaction do not roll back commands already executed successfully, only queueing errors prevent execution entirely via EXECABORT.
Anyone who needs genuine conditional logic between multiple Redis operations, or expects maximum atomicity on errors, should reach for Lua scripts with EVAL. For simple, unconditional command sequences, MULTI/EXEC combined with WATCH remains the most direct and easiest to understand tool for concurrency-safe operations in Redis.
Redis transactions with MULTI/EXEC, the essentials at a glance
Command queueing
MULTI starts the queueing mode, every command gets confirmed with QUEUED, execution only happens with EXEC.
WATCH for optimistic locking
Watched keys that change before EXEC automatically abort the transaction. The application must retry the cycle.
No rollback
A runtime error in one command does not stop the remaining commands of the transaction. Error checking is up to the client.
Alternative when needed
For conditional logic between commands, Lua scripts with EVAL are the more robust and usually faster choice.