Redis Error Guide: 'EXEC without MULTI' — Fix Broken Transaction Sequencing and Pooled Connections
Fix ERR EXEC without MULTI in Redis: understand MULTI/EXEC transaction state and how connection pooling and reconnects desync transaction commands.
- #redis
- #database
- #troubleshooting
- #errors
Stuck on this Redis error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
Redis returns EXEC without MULTI when it receives an EXEC on a connection that is not currently inside a transaction — that is, no matching MULTI opened a transaction on that same connection first. Redis transactions are strictly per-connection and stateful: MULTI opens a queue, subsequent commands are queued, and EXEC runs them. Sending EXEC when no queue is open is a protocol/state error.
The literal error clients receive:
(error) ERR EXEC without MULTI
This is almost never a Redis bug; it is a sequencing or connection-management bug in the client. The usual causes are a MULTI that was never sent (or was rejected), a transaction already ended by a prior EXEC/DISCARD, or — most insidiously — a connection pool that sent MULTI on one connection and EXEC on a different one.
Symptoms
EXECfails withEXEC without MULTI, often intermittently under concurrency.- The error correlates with connection-pool reuse, reconnects, or pipelining.
- A prior command in the intended transaction errored, and the client sent
EXECanyway.
redis-cli EXEC
(error) ERR EXEC without MULTI
# Correct sequence on one connection
redis-cli -3 <<'EOF'
MULTI
SET a 1
INCR a
EXEC
EOF
Common Root Causes
1. EXEC sent on a different pooled connection than MULTI
The client library checked out one connection for MULTI, then a different connection from the pool for EXEC. The second connection never saw MULTI.
2. MULTI was never sent or was rejected
The MULTI command was skipped by a code path, or rejected (e.g. MULTI nested / connection reset), so EXEC arrives with no open transaction.
3. The transaction already ended
A prior EXEC or DISCARD already closed the transaction, and a duplicate EXEC followed.
4. Reconnect mid-transaction
The connection dropped and reconnected between MULTI and EXEC; the new socket has no transaction state, so EXEC fails.
5. Manual pipelining that reorders commands
Hand-built pipelines that interleave commands from different logical operations can deliver EXEC before its MULTI.
Diagnostic Workflow
Step 1: Reproduce the correct sequence on one connection
redis-cli
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET a 1
QUEUED
127.0.0.1:6379> EXEC
1) OK
If this works but your app fails, the bug is in how the app manages the connection, not in Redis.
Step 2: Confirm MULTI and EXEC share one connection
Inspect the client library’s transaction/pool usage. The MULTI, all queued commands, and EXEC must run on the same physical connection, held for the transaction’s duration.
Step 3: Watch the actual command stream
# Briefly, on a non-prod or low-traffic instance only
redis-cli MONITOR | grep -iE 'MULTI|EXEC|DISCARD'
Look for an EXEC with no preceding MULTI on the same client, or MULTI/EXEC from different client addresses/ports.
Step 4: Check for reconnects and errors between MULTI and EXEC
redis-cli INFO stats | grep -E 'total_connections_received|rejected_connections'
sudo journalctl -u redis-server --no-pager | grep -iE 'MULTI|EXEC|closing' | tail
Step 5: Prefer a transaction/Lua abstraction
Confirm whether the code hand-writes MULTI/EXEC or uses the library’s transaction helper (pipeline-with-transaction), which pins the connection for you.
Example Root Cause Analysis
A payment service intermittently logs ERR EXEC without MULTI under load. It uses a connection pool and hand-writes transactions: it calls client.execute("MULTI"), queues writes, then client.execute("EXEC") — but each execute checks out a fresh connection from the pool.
MONITOR on a canary shows the smoking gun: MULTI and EXEC arrive from different client ports:
1720000000.1 [0 10.0.0.5:51122] "MULTI"
1720000000.2 [0 10.0.0.5:51122] "SET" "txn:9" "pending"
1720000000.3 [0 10.0.0.5:51188] "EXEC" <-- different port!
Under low load the pool happened to hand back the same connection, so it worked; under concurrency it handed out a different one, and the second connection had no open MULTI. The fix was to use the library’s transaction/pipeline abstraction that pins one connection for the whole transaction, rather than issuing MULTI and EXEC as independent pool checkouts:
# pseudocode — pin one connection for the transaction
with pool.pipeline(transaction=True) as tx:
tx.multi()
tx.set("txn:9", "pending")
tx.incr("txn:count")
tx.execute() # MULTI...EXEC all on one connection
After switching to the pinned-connection transaction API the error disappeared entirely, because MULTI, the queued commands, and EXEC were guaranteed to travel on the same socket.
Prevention Best Practices
- Always run
MULTI, the queued commands, andEXECon the same connection; use your client library’s transaction/pipeline helper rather than issuing them as separate pool checkouts. - Never let a connection return to the pool while a transaction is open; pin it for the transaction’s lifetime.
- Handle reconnects by abandoning and restarting the transaction — transaction state does not survive a new socket.
- For complex atomic logic, prefer a Lua script or a Redis Function, which is atomic without cross-command connection state.
- Check for and handle errors on the queued commands before calling
EXEC(a bad queued command aborts the transaction withEXECABORT). - Feed the command stream into the free incident assistant, and browse more Redis guides.
Quick Command Reference
# Correct, single-connection transaction
redis-cli <<'EOF'
MULTI
SET a 1
INCR a
EXEC
EOF
# Observe MULTI/EXEC pairing and client ports (low-traffic only)
redis-cli MONITOR | grep -iE 'MULTI|EXEC|DISCARD'
# Reconnect / rejection signals
redis-cli INFO stats | grep -E 'total_connections_received|rejected_connections'
# Abandon a transaction cleanly
redis-cli DISCARD
Conclusion
EXEC without MULTI means Redis received an EXEC on a connection with no open transaction. The typical root causes are:
EXECsent on a different pooled connection thanMULTI.MULTInever sent or rejected.- A transaction already closed by a prior
EXEC/DISCARD. - A reconnect between
MULTIandEXECthat reset transaction state. - Manual pipelining that reordered the commands.
Because transactions are per-connection state, the durable fix is to pin one connection for the whole MULTI…EXEC sequence via your client’s transaction helper — or move the atomic logic into a Lua script/Function, which needs no cross-command connection state at all.
Fixed it? Get 500 Redis & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.