Chapter 13 · Transactions, WATCH, Optimistic Locking, and Atomicity
DISCARD, Command Errors, Runtime Errors, and Partial Semantic Surprises Inside EXEC
Separate queue-time rejection, EXEC-time command errors, WATCH aborts, and DISCARD so failure handling is based on actual Redis state instead of rollback assumptions.
Learning outcomes
“The transaction failed” is too vague for production diagnosis. Redis exposes several different failure classes: a command may be rejected while queueing, a queued command may fail at runtime inside EXEC, WATCH may abort execution before any queued command runs, or the client may explicitly DISCARD the queue. Each leaves a different final state.
Classify queue-time errors, runtime EXEC errors, WATCH aborts, and DISCARD as distinct outcomes.
Explain why runtime errors can coexist with successful writes in the same EXEC reply array.
Use DISCARD correctly and explain why it cannot undo commands after EXEC.
Inspect final key state rather than inferring rollback from a client exception.
Account for client-library error shaping and persistence/topology consequences separately.
All Chapter 13 mandatory labs reuse the disposable Chapter 01
environment: Redis Open Source 8.10.1 from Docker
Official Image redis:8.10.1, container
atlasmart-redis-ch01, standalone topology, host
publication 127.0.0.1:6379, TLS disabled only
because traffic stays on loopback, default ACL user disabled,
named users academy-admin and
atlasmart-app, logical database 0, AOF with
appendfsync everysec plus RDB snapshots,
persistent /data, and no explicit
maxmemory limit or eviction policy. Transaction
exercises use academy-admin because the Chapter
01 application ACL was intentionally not broadened with the
separate @transaction category; production
applications should grant only the commands and key patterns
they need. Fixtures stay under atlasmart:ch13:*.
Client-oriented exercises pin redis-py 8.1.0 on
Python 3.12 and use one pipeline/context per watched
transaction so connection-scoped WATCH state is not
accidentally lost.
1. Four outcomes that must not share one error label
| Outcome | EXEC behavior | Redis writes from queued transaction |
|---|---|---|
| Queue-time command error | EXECABORT | None |
| Runtime command error | EXEC returns array containing error reply | Other queued commands still execute |
| WATCH conflict | EXEC returns nil/null | None |
| DISCARD before EXEC | No EXEC; queue cleared | None |
The application should log these separately because their retry safety and final-state implications are different.
2. Queue-time rejection: the transaction is invalid before EXEC
Redis validates enough command syntax while queueing to reject commands with the wrong arity or unknown command name. On modern Redis, a queue-time error causes the later EXEC to abort the transaction rather than execute the rest of the queue.
docker exec -it -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin# Keep MULTI/WATCH and the commands they protect on this same redis-cli connection.
UNLINK atlasmart:ch13:l3:q-a atlasmart:ch13:l3:q-bMULTISET atlasmart:ch13:l3:q-a 1INCR atlasmart:ch13:l3:q-a too-many-arguments# Expected immediately: an ERR reply, not QUEUED.SET atlasmart:ch13:l3:q-b 2EXEC# Expected: EXECABORT; neither q-a nor q-b is written.EXISTS atlasmart:ch13:l3:q-a atlasmart:ch13:l3:q-b
3. Runtime error: one reply is an error, siblings still run
Runtime validation may depend on the actual value type. The command can therefore queue successfully and fail only during EXEC. Redis does not roll back other successful commands.
SET atlasmart:ch13:l3:number 10MULTIINCR atlasmart:ch13:l3:numberLPUSH atlasmart:ch13:l3:number wrong-type-on-purposeSET atlasmart:ch13:l3:marker still-runsEXEC# Expected replies: integer 11; WRONGTYPE error; OKMGET atlasmart:ch13:l3:number atlasmart:ch13:l3:marker# Expected: "11", "still-runs"
4. A client exception can hide the rest of the EXEC array
Client libraries choose how to surface Redis error replies.
redis-py normally raises a response error for a transactional
pipeline when raise_on_error=True. For a diagnostic
lab, raise_on_error=False lets you inspect the
complete reply list and then verify Redis state. This is an
observability choice, not a way to make the error harmless.
import redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", decode_responses=True)key = "atlasmart:ch13:l3:number"r.set(key, "10")with r.pipeline(transaction=True) as pipe: pipe.incr(key) pipe.lpush(key, "wrong-type-on-purpose") pipe.set("atlasmart:ch13:l3:marker", "still-runs") replies = pipe.execute(raise_on_error=False)for i, reply in enumerate(replies, 1): print(i, type(reply).__name__, repr(reply))print("number=", r.get(key))print("marker=", r.get("atlasmart:ch13:l3:marker"))
5. WATCH abort is pre-execution, not an EXEC runtime error
A WATCH conflict prevents the queued transaction from running at
all. In raw Redis the result is nil/null; redis-py converts that
condition into WatchError. Do not group it with
WRONGTYPE, because retrying after re-reading can be correct for
WATCH while blindly retrying a deterministic type error is
usually useless.
| Failure | Retry after re-read? | Likely repair |
|---|---|---|
| WATCH conflict | Often, with budget/backoff | Re-read; possibly redesign hot key |
| WRONGTYPE runtime error | Usually not until data/model fixed | Correct type/schema assumption |
| Queue-time syntax error | No | Fix command construction/client bug |
| Timeout/connection loss around EXEC | Ambiguous outcome possible | Use idempotent design/reconciliation; inspect authoritative state |
6. DISCARD cancels only queued, not already executed, work
DISCARD is useful when the application changes its mind before EXEC. It has O(N) work in the number of queued commands because Redis clears the queue. It cannot undo a transaction that already executed.
SET atlasmart:ch13:l3:discard 5MULTIINCR atlasmart:ch13:l3:discardINCR atlasmart:ch13:l3:discardDISCARDGET atlasmart:ch13:l3:discard# Expected: "5"
7. Command ordering can create semantic surprises even without errors
Commands inside EXEC run in queue order. Later commands see the effects of earlier commands in the same transaction. That can be useful, but it also means a copied command sequence may have different semantics if reordered.
SET atlasmart:ch13:l3:ordered 5MULTIINCR atlasmart:ch13:l3:orderedGET atlasmart:ch13:l3:orderedINCRBY atlasmart:ch13:l3:ordered 10GET atlasmart:ch13:l3:orderedEXEC# Expected replies reflect 6 first, then 16; GET replies show those intermediate transaction states.
8. “No interleaving” does not make slow work safe
EXEC time depends on the complexity of the queued commands. A transaction that performs large O(N) work can hold the server’s command-processing path for longer and increase tail latency for other clients. Atomicity is not a performance exemption. Keep transactions short and bounded; do not hide scans, huge range results, or expensive Search operations inside them merely because they are allowed on one topology.
9. Search and Cluster add topology-sensitive failure modes
Current Redis documentation notes that Search commands inside MULTI/EXEC are synchronous on standalone/single-shard deployments, while multi-shard Search commands can be rejected because their coordinator needs asynchronous fan-out. Redis Open Source Cluster also restricts transaction keys to one slot. Therefore a transaction that works in this Chapter 13 standalone lab is not automatically portable to every distributed topology.
10. Persistence and connection-loss ambiguity
If a client disconnects before sending EXEC, queued operations do not run. Once EXEC is sent, the client can lose the response even though Redis may have executed the transaction. That creates an application-level “unknown outcome” problem. Design retryable/idempotent state transitions and reconciliation instead of assuming “no reply means no write.” Separately, AOF/RDB/fsync determine crash durability.
11. Reproducible lab: classify and record each outcome
Run one example from each failure class and write down three facts: what the client observed, whether EXEC ran, and the final Redis state. That triad is more useful than a generic exception string.
EXISTS atlasmart:ch13:l3:q-a atlasmart:ch13:l3:q-bMGET atlasmart:ch13:l3:number atlasmart:ch13:l3:markerGET atlasmart:ch13:l3:discardGET atlasmart:ch13:l3:ordered# Expected evidence differs by failure class; preserve it in test output/logs.
12. Repair pattern: validate invariants before transaction construction
Type/schema checks, command generation tests, bounded payloads, and explicit postconditions prevent many runtime surprises. When you intentionally rely on multiple command replies, handle the complete EXEC array. If one error must invalidate the whole logical operation, redesign the operation rather than relying on rollback Redis does not provide.
13. Cleanup
UNLINK atlasmart:ch13:l3:q-a atlasmart:ch13:l3:q-b atlasmart:ch13:l3:number atlasmart:ch13:l3:marker atlasmart:ch13:l3:discard atlasmart:ch13:l3:ordered
14. Production judgment
Model transaction outcome as a state machine, not a boolean. Distinguish validation/queue failures, conditional aborts, runtime command errors, client timeouts, and successful execution. Emit metrics for each class and verify final Redis state where a retry could duplicate effects. Keep queues bounded and topology-compatible. This becomes especially important before Chapter 14 moves logic server-side, because scripts/functions are atomic but still need explicit error, timeout, versioning, and rollback/reconciliation design.
Check your understanding
- What final state follows a queue-time syntax error and later EXEC?
- What final state follows one runtime WRONGTYPE error inside EXEC?
- Can DISCARD undo a transaction after EXEC?
- Why inspect final state after a client timeout around EXEC?
- Why is a WATCH conflict a different retry class from WRONGTYPE?
Review the answers
The transaction is aborted; queued writes do not execute.
Other successfully queued commands still execute; there is no automatic rollback.
No. It only clears queued work before execution.
The client may not know whether EXEC reached the server, so the outcome can be ambiguous.
WATCH means observed state changed and re-reading may make a retry valid; WRONGTYPE usually indicates a deterministic data/model error.
15. Summary and next step
You now have a precise failure vocabulary: EXECABORT for queue construction failure, error elements for runtime command failure, null/nil for WATCH invalidation, and DISCARD for intentional cancellation. Lesson 4 uses that vocabulary to choose the smallest correct primitive instead of reaching for transactions automatically.
Authoritative references
- Redis Open Source 8.10 release notes
- Redis 8.10 command reference
- Redis transactions
- MULTI
- EXEC
- DISCARD
- WATCH
- UNWATCH
- Redis multi-key operations
- Redis pipelining
- redis-py pipelines and transactions
- redis-py documentation
- SET
- DELEX
- MSETNX
- INCR
- HINCRBY
- EVAL
- FCALL
- Redis ACLs
- Redis persistence
- Redis replication
- Redis licenses
- Search commands in transactions and scripts