The Problem

Redis transactions using MULTI/EXEC fail unexpectedly. Commands queued in a transaction abort, or EXEC returns null indicating the transaction was discarded. You see errors like:

bash
(error) EXECABORT Transaction discarded because of previous errors

or EXEC returns (nil) when using WATCH, meaning the transaction was aborted due to key changes.

Understanding Redis Transactions

Redis transactions work differently from traditional database transactions:

  1. 1.MULTI - Start transaction, queue subsequent commands
  2. 2.Commands queued - Not executed immediately
  3. 3.EXEC - Execute all queued commands atomically
  4. 4.DISCARD - Cancel transaction without executing

Key differences from SQL transactions: - No rollback on command failure within EXEC - All commands execute even if one fails - Atomicity means no other commands execute between queued commands

Diagnosis Commands

Check Transaction Status

bash
# No direct command to check transaction state
# Use CLIENT LIST to see clients in transaction
redis-cli CLIENT LIST | grep -E 'flags=.*M'

The M flag indicates a client with an active MULTI transaction.

Debug Transaction Issues

bash
redis-cli DEBUG SLEEP 1  # To see transaction state briefly

Monitor Commands

bash
redis-cli MONITOR

Watch MULTI, queued commands, and EXEC/DISCARD in real-time.

Common Transaction Abort Scenarios

Scenario 1: Command Syntax Error Before EXEC

Problem: A command with syntax error is queued. EXEC aborts the entire transaction.

bash
redis-cli
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> SET key2    # Missing value - syntax error
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors

Solution: Validate commands before queuing or handle errors:

```bash # Use redis-cli --eval for script-based transactions # Or validate command syntax in application before sending

redis-cli MULTI OK redis-cli SET key1 value1 QUEUED redis-cli SET key2 value2 QUEUED redis-cli EXEC 1) OK 2) OK ```

Scenario 2: WATCH Key Modified by Another Client

Problem: Optimistic locking with WATCH fails when another client modifies the watched key.

```bash # Client 1 redis-cli WATCH counter OK redis-cli GET counter "10" redis-cli MULTI OK redis-cli SET counter 11 QUEUED

# Client 2 (while Client 1 is queuing) redis-cli SET counter 20 OK

# Client 1 redis-cli EXEC (nil) # Transaction aborted - counter was modified ```

Solution: Retry the transaction with updated value:

```python import redis

r = redis.Redis()

def increment_counter(key, max_retries=3): for attempt in range(max_retries): try: # Watch the key r.watch(key)

# Get current value current = r.get(key) if current is None: current = 0 else: current = int(current)

# Start transaction with r.pipeline() as pipe: pipe.multi() pipe.set(key, current + 1) pipe.execute() return current + 1

except redis.WatchError: # Key was modified, retry continue except Exception as e: r.unwatch() raise

raise Exception("Transaction failed after max retries") ```

Scenario 3: Type Mismatch in Transaction

Problem: Commands fail due to wrong key types, but EXEC still runs all commands.

bash
redis-cli SET mylist "string_value"  # Create string key
OK
redis-cli MULTI
OK
redis-cli LPUSH mylist element1  # Will fail - wrong type
QUEUED
redis-cli SET other_key value
QUEUED
redis-cli EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) OK

Note: Unlike syntax errors, runtime errors don't abort the transaction. The SET still executes.

Solution: Check key types before transaction:

```bash redis-cli TYPE mylist # If returns "string", don't use list commands

# Or use TYPE check in transaction: redis-cli MULTI OK redis-cli TYPE mylist QUEUED redis-cli LPUSH mylist element1 QUEUED redis-cli EXEC # Check TYPE result before proceeding ```

Scenario 4: Transaction Timeout

Problem: Long transactions block other operations.

Diagnosis:

bash
redis-cli CLIENT LIST | grep -E 'flags=.*M'
redis-cli CONFIG GET timeout

Solution: Keep transactions short:

```bash # Don't do slow operations in transaction redis-cli MULTI OK redis-cli KEYS * # DON'T - blocks and is slow QUEUED redis-cli EXEC

# Instead, use short atomic operations redis-cli MULTI OK redis-cli SET key1 value1 QUEUED redis-cli SET key2 value2 QUEUED redis-cli INCR counter QUEUED redis-cli EXEC ```

Transaction Pattern Best Practices

Pattern 1: Optimistic Locking with Retry

```python import redis import time

r = redis.Redis()

def safe_transfer(from_key, to_key, amount, max_retries=10): """ Safely transfer amount between keys using WATCH. """ for attempt in range(max_retries): try: r.watch(from_key, to_key)

from_balance = r.get(from_key) if from_balance is None: from_balance = 0 else: from_balance = int(from_balance)

if from_balance < amount: r.unwatch() raise ValueError("Insufficient balance")

to_balance = r.get(to_key) if to_balance is None: to_balance = 0 else: to_balance = int(to_balance)

with r.pipeline() as pipe: pipe.multi() pipe.set(from_key, from_balance - amount) pipe.set(to_key, to_balance + amount) pipe.execute() return True

except redis.WatchError: # Retry with backoff time.sleep(0.1 * attempt) continue

return False ```

Pattern 2: Atomic Multi-Key Operations

```bash # Without transaction - race condition possible redis-cli SET user:1:balance 100 redis-cli SET user:2:balance 200 redis-cli SET user:1:balance 50 # Could happen between above redis-cli SET user:2:balance 250

# With transaction - atomic redis-cli MULTI OK redis-cli SET user:1:balance 100 QUEUED redis-cli SET user:2:balance 200 QUEUED redis-cli EXEC 1) OK 2) OK ```

Pattern 3: Conditional Transactions

```lua -- Lua script for conditional operation (alternative to WATCH) local key = KEYS[1] local expected = ARGV[1] local new_value = ARGV[2]

local current = redis.call('GET', key) if current == expected then redis.call('SET', key, new_value) return 1 else return 0 end ```

bash
redis-cli --eval conditional_set.lua key1 , expected_value new_value

Pattern 4: Pipelining vs Transactions

```python # Pipeline without transaction - faster, no atomicity pipe = r.pipeline() pipe.set('key1', 'value1') pipe.set('key2', 'value2') pipe.set('key3', 'value3') results = pipe.execute()

# Pipeline with transaction - atomic, slower pipe = r.pipeline() pipe.multi() # Start transaction pipe.set('key1', 'value1') pipe.set('key2', 'value2') pipe.set('key3', 'value3') results = pipe.execute() ```

Debugging Transaction Issues

Step 1: Monitor in Real-Time

bash
redis-cli MONITOR

Send transactions and observe: - MULTI command - Queued commands - EXEC or DISCARD - Results

Step 2: Check Client State

bash
# Find clients with open transactions
redis-cli CLIENT LIST | awk '/flags=.*M/ {print}'

Step 3: Kill Stuck Transactions

```bash # Find stuck client ID redis-cli CLIENT LIST | grep 'flags=.*M'

# Kill the client redis-cli CLIENT KILL ID <client_id> ```

Step 4: Validate Command Syntax

bash
# Test command before using in transaction
redis-cli SET key value  # If this works, will work in transaction
redis-cli LPUSH key value  # Test type compatibility

Common Pitfalls

  1. 1.Not handling WatchError - Application crashes on aborted transaction
  2. 2.Using KEYS in transaction - Extremely slow, blocks server
  3. 3.Not checking TYPE - Wrong type errors persist through EXEC
  4. 4.No retry logic - Single WATCH failure aborts entire operation
  5. 5.Too many commands - Long transactions increase collision chance

Verification

Test transaction behavior:

```bash # Test simple transaction redis-cli MULTI OK redis-cli SET test_key test_value QUEUED redis-cli INCR test_counter QUEUED redis-cli EXEC # Should return: 1) OK 2) (integer) 1

# Test WATCH abort redis-cli WATCH test_key OK redis-cli MULTI OK redis-cli SET test_key new_value QUEUED # From another terminal: redis-cli SET test_key modified redis-cli EXEC (nil) # Transaction aborted ```

Monitoring Transactions

```bash #!/bin/bash # Monitor for clients stuck in transactions

while true; do MULTI_CLIENTS=$(redis-cli CLIENT LIST | grep -c 'flags=.*M')

if [ "$MULTI_CLIENTS" -gt 5 ]; then echo "$(date): WARNING: $MULTI_CLIENTS clients in transaction" redis-cli CLIENT LIST | grep 'flags=.*M' fi

sleep 5 done ```