autopipelining: commands fail during cluster topology changes due to pipeline-level MOVED handling
TLDR: enableAutoPipelining does not work correctly with dynamic clusters (resharding, slot migration, failover). If your cluster topology changes at runtime, autopipelining will cause command failures that would not occur without it.
Summary
When enableAutoPipelining is used with Redis.Cluster, topology changes (slot migration, reshard, failover) can cause entire batches of autopipelined commands to fail. The core issue is that autopipelines group commands for different slots into a single pipeline, but MOVED errors are handled at the pipeline level rather than per-command.
Background
Autopipelining groups commands by node: all commands whose slots are served by the same set of nodes end up in one pipeline. A single autopipeline typically contains commands targeting many different slots.
For individual (non-pipelined) commands, the cluster client handles MOVED errors transparently - it catches the redirect, updates routing, and retries the command to the correct node. The application never sees MOVED errors.
Problem
Consider a topology change where slot 200 migrates from Node A to Node C, while slot 100 stays on Node A:
- Autopipelining groups commands for slots 100 and 200 together (both served by Node A at the time of batching)
- The pipeline sends everything to Node A
- Slot 100 commands succeed, slot 200 commands come back with
MOVED 200 NodeC:port
What happens next depends on the command mix:
Failure Mode 1: Pipelines with writes
If any successful command is a write (SET, INCR, etc.), the pipeline considers itself non-retriable and returns individual results as-is. Successful commands resolve normally, but MOVED commands bubble up as raw errors to the application.
This is unexpected - applications don't expect to see MOVED errors since the cluster client handles them transparently for individual commands.
In this path the entire MOVED handling block is bypassed: no slot mapping updates, no refreshSlotsCache() call. The stale slot map persists, so subsequent autopipeline batches keep getting routed to the wrong node and keep hitting the same MOVED errors. Since slotsRefreshInterval is not configured by default, the map stays stale until something external triggers a refresh (e.g., a non-autopipelined command hitting MOVED).
Failure Mode 2: Read-only pipelines (worse)
If all successful commands are reads, the pipeline considers itself retriable. It parses the commonError message, updates the mapping for that one slot (slot 200 now points to Node C), and re-runs exec(). But the slot map is now only partially corrected - the moved handler calls refreshSlotsCache(), but it's async (sends CLUSTER SLOTS to a node), while the retry exec() runs synchronously right after, before the full topology arrives.
On retry, generateMultiWithNodes sees that slots 100 and 200 now belong to different allocation groups and rejects the pipeline with "All keys in the pipeline should belong to the same slots allocation group".
All commands fail - including the reads that already succeeded on the first attempt. The retry made things strictly worse.
While a pipeline is going through retries, _runningAutoPipelines keeps the slot key locked. New commands for the same slot queue up. After the current pipeline finally fails, the queued commands form a new batch that hits the same stale routing and fails again.
Source: redis/ioredis