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.

Advanced170–220 minutesTransactions and concurrencyRedis Open Source 8.10.1Free/local-firstLast reviewed: September 6, 2026

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.

01

Classify queue-time errors, runtime EXEC errors, WATCH aborts, and DISCARD as distinct outcomes.

02

Explain why runtime errors can coexist with successful writes in the same EXEC reply array.

03

Use DISCARD correctly and explain why it cannot undo commands after EXEC.

04

Inspect final key state rather than inferring rollback from a client exception.

05

Account for client-library error shaping and persistence/topology consequences separately.

Exact lab baseline

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.

Terminal · open one persistent redis-cli connection
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.
redis-cli · queue-time error
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.

redis-cli · runtime WRONGTYPE inside EXEC
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.

Python · inspect every EXEC reply explicitly
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.

redis-cli · correct DISCARD semantics
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.

redis-cli · observe in-transaction order
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.

redis-cli · final evidence bundle
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

redis-cli · bounded Lesson 3 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

  1. What final state follows a queue-time syntax error and later EXEC?
  2. What final state follows one runtime WRONGTYPE error inside EXEC?
  3. Can DISCARD undo a transaction after EXEC?
  4. Why inspect final state after a client timeout around EXEC?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.