go-redis/redis · error
redis: unexpected EXEC reply %q
Error message
redis: unexpected EXEC reply %q
What it means
Raised by readTxPipelineReplies when the EXEC reply line for a cluster MULTI..EXEC transaction is not a RESP array (does not start with '*'). A successful EXEC must return an array of per-command results; any other aggregate type means the protocol stream is misaligned or the server sent something unexpected.
Source
Thrown at osscluster.go:2550
cmd.SetErr(err)
if firstFatal == nil {
firstFatal = err
}
}
// EXEC reply. ReadLine parses error lines into typed errors, so a non-nil
// err means EXEC returned an error rather than the result array.
c.txProcessPush(ctx, node, cn, rd)
line, err := rd.ReadLine()
if err != nil {
if !isRedisError(err) {
return c.txReadFatal(err) // IO error
}
return c.classifyExecError(err, firstRedirect, firstFatal)
}
if line[0] != proto.RespArray {
err := fmt.Errorf("redis: unexpected EXEC reply %q", line)
setCmdsErr(cmds, err)
// A non-array aggregate reply may carry an unread payload.
return &txOutcome{kind: txFatal, err: err, unreadReplies: true}
}
// Success: read the N command results.
if err := node.Client.pipelineReadCmds(ctx, cn, rd, cmds); err != nil && !isRedisError(err) {
return c.txReadFatal(err) // IO error mid-results
}
return &txOutcome{kind: txSuccess}
}
func (c *ClusterClient) txProcessPush(ctx context.Context, node *clusterNode, cn *pool.Conn, rd *proto.Reader) {
if err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
}
}
View on GitHub (pinned to 36d97525cd)
Solutions
- Retry the transaction once on a fresh connection (the pool will discard the dirty one).
- Verify Protocol matches what the server/proxy supports (Protocol 3 vs 2).
- If a proxy is in front, ensure it forwards MULTI/EXEC replies unchanged; report persistent occurrences upstream.
Defensive patterns
Strategy: retry
Try / catch
if err := cmd.Err(); err != nil && strings.Contains(err.Error(), "unexpected EXEC reply") {
// retry the MULTI/EXEC on a fresh connection; verify Protocol matches server/proxy
} Prevention
- Match Options.Protocol to what the server/proxy supports.
- Ensure any proxy forwards MULTI/EXEC replies verbatim.
- Set MaxRetries > 0 so transient protocol hiccups retry automatically.
When it happens
Trigger: A protocol-level mismatch during a cluster tx pipeline: a server that returns a non-array aggregate at EXEC (e.g. a nil bulk reply, an error the reader did not classify, or a truncated frame). All commands in the batch are marked with this error and the connection is flagged as having unread replies.
Common situations: RESP2/RESP3 mismatch, a proxy or fork that rewrites MULTI replies, a corrupted TCP stream, or an upstream bug. Usually accompanied by connection churn since the conn is considered dirty.
Related errors
- redis: tx pipeline produced no outcome
- redis: expected '*', but got line %q
- redis: cannot pipeline command %q with request policy ReqAll
- redis: no valid results to aggregate for max operation
- redis: no valid results to aggregate for logical AND operati
AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06).
Data as JSON: /data/errors/5b7c5633583f4819.json.
Report an issue: GitHub.