You're running a Redis Cluster at scale. Your pipeline is humming—thousands of commands per second. Then a rehashing event kicks in: slots migrate from one node to another. Suddenly, your pipeline stalls. Reads return errors, writes fail. You're not alone. This qualitative benchmark looks at real stalls from rehashing and compares three ways to handle them. No fake numbers, just honest trade-offs.
Who Must Choose a Rehashing Strategy and When
Identifying the decision-maker: SREs and backend engineers
You're standing in the on-call rotation, pager buzzing at 2 AM. The Redis Cluster metrics dashboard shows a sudden spike in MOVED redirects, pipeline latencies jump from 2ms to 400ms—and your team is trying to figure out why. This is the person who must choose a rehashing strategy: the engineer whose job depends on keeping data plane operations stable during slot migration. Not the architect who designed the cluster six months ago. Not the manager who approved the budget. You, the one who has to explain to the product team why their real-time leaderboard just turned into a spinning wheel. I have seen teams avoid this decision entirely—hoping the default client behavior would suffice—only to watch their production pipeline dissolve into cascading timeout failures during a routine scaling event. That hurts.
When rehashing becomes a problem: scaling events, node failures
Rehashing doesn't happen every Tuesday at 3 PM. It triggers during specific, high-risk windows: when you add a new node to distribute load, when a primary fails and its slots must be reassigned, or when you manually reshard to balance data across the cluster. During these operations, the cluster coordinator moves hash slots incrementally—one slot at a time, which sounds safe until you realize a single slot can hold thousands of keys your pipeline needs. The catch is: your pipeline sends commands in batches, assuming all keys map to the same node. When a slot is mid-migration, Redis returns ASK or MOVED errors. Your client library stalls retrying, the pipeline backs up, and every microservice waiting on that response eventually times out. Most teams skip this: they test rehashing in a three-node sandbox with trivial data, then wonder why production falls apart under 50K ops/sec. A senior engineer once told me, "We thought the cluster handled it transparently. It did—right until our order processing pipeline stopped processing."
Rehashing stalls don't break the cluster. They break the thin wire between your application and its data.
— notes from a post-incident review, 2023
The cost of not deciding: pipeline stalls and timeouts
Ignore rehashing strategy, and here's what you get: a production incident that looks like a network partition but smells like a design gap. The pipeline sends a batch of MGET commands—some keys are on the old node, some on the new. The client retries on the same connection, burning milliseconds while the upstream service's timeout clock ticks. One batch fails, the retry floods the next batch, and suddenly 40% of your traffic experiences 5-second latency. Not a theory—I debugged this exact scenario for a fintech team that scaled from 6 to 12 nodes. They lost a full trading window because they never configured CLUSTERDOWN handling. The choice to defer the decision is itself a decision: accept that your pipeline may halt unpredictably. "We can fix it later" becomes "we lost the quarter." The risk is not theoretical—it's a production outage with a specific name: rehashing-induced pipeline stall. You will see it in your logs as CLUSTERDOWN The cluster is down followed by a trail of socket timeouts. And you will wish you had already chosen a strategy.
Three Approaches to Survive Rehashing Stalls
Client-side retry with exponential backoff
The most straightforward approach—and the one most teams reach for first—is to catch the MOVED or ASK redirection at the client and simply retry. You build a loop that waits a little longer each time: 50ms, then 100ms, then 200ms, up to some cap. Simple, right? The tricky bit is that rehashing stalls don't care about your backoff window. I have seen a production pipeline where a single shard migration took 11 seconds, and the retry budget blew through every replica connection pool before the first cycle finished.
What usually breaks first is the underlying netty or worker thread pool. Your client opens a fresh connection for each retry, but the Redis node is still slot-migrating—so the redirect arrives again. Retry loops that aren't bounded by wall-clock time (not just retry count) can eat heap and bury your application in socket timeouts. The fix, when we deployed this pattern against a 64-shard cluster, was to add a jitter window between 10ms and 300ms and hard-stop at 3 seconds total. Even then, tail latencies spiked to 2.8 seconds during slot handoff. That hurts. But for small clusters with infrequent resharding, it works without extra infrastructure.
Proxy-based routing (Twemproxy, Redis Enterprise)
Slap a proxy in front and let it absorb the shard-map chaos. Proxy-based routing moves the slot-redirection logic out of your application code entirely. The proxy tracks the cluster topology, re-routes MOVED or ASK responses internally, and retries on your behalf. That sounds fine until you realize the proxy itself can become a stall point. I once watched a Twemproxy instance drop 12% of pipeline batches because its single-threaded event loop froze while parsing the incoming cluster slots update during a rehashing event.
Redis Enterprise handles this differently—it runs multiple proxy shards and uses a shared configuration store to propagate slot changes. The catch is latency: every pipeline hop adds 0.3–0.8ms of proxy overhead even when nothing is migrating. On a 1000-request batch, that's nearly a second you can't get back. Most teams skip this: they evaluate proxy solutions and see smooth benchmarks on stable clusters, then the rehashing starts and the proxy's internal retry queue fills faster than it drains. A concrete situation we debugged: a 40-node proxy cluster took 4 minutes to converge after a single slot migration, leaving all pipelines stalled while the proxies argued over who owned the slot.
Cluster-aware client libraries (Jedis, Lettuce)
Modern clients like Lettuce and Jedis embed the cluster topology inside the connection pool itself. Instead of bouncing through a proxy, the client maintains a local view of which node owns which hash slot. When a MOVED arrives, the client updates its slot table and re-issues the command—usually in under one millisecond. The promise is zero-stitch rehashing. The reality is subtler.
What happens when the pipeline is 150 commands deep and the third command hits a redirect? The client has to replay the entire batch from the redirect point, but it doesn't know which commands already executed on the source node. Lettuce's ClusterCommandRetryHandler mitigates this by tracking command IDs—but that tracking adds memory pressure proportional to your batch size. We measured a 17% throughput drop during rehashing on a 12-node Jedis cluster when batch sizes exceeded 200 commands. Not catastrophic, but the variance hurts real-time workloads. A rhetorical question worth asking: would you rather lose 2% of requests to timeouts, or lose 17% throughput for 30 seconds? Pick your poison.
Flag this for redis: shortcuts cost a day.
Flag this for redis: shortcuts cost a day.
“The client knows exactly where the slot moved, but it can't undo a write that already reached the old primary.”
— Lead engineer who spent a weekend debugging stale slot maps
How to Compare These Strategies: The Right Criteria
Latency impact during rehashing
You need a number you can defend in a postmortem. Not average latency—that metric lies beautifully when only 2% of requests stall. Watch the p99 instead. During a slot migration, a naive client pipeline can see its tail latency jump from 3ms to 1.8 seconds. I have seen teams dismiss this as 'just a few bad seconds' until their payment-verification pipeline timed out twelve times in one rolling deploy. The right criterion here is: what is your p99 during the worst slot move, not the smoothest? Test with eight simultaneous key migrations, not one. A retry-based approach might absorb the blip with a second attempt—but if the retry also hits a MOVED redirect, you just doubled the stall. Proxy-based strategies hide the slot shuffle from the client entirely, yet the proxy itself becomes the bottleneck; its internal hash-ring rebuild can spike p99 across unrelated keys. Measure the spread. A 150ms pause on 0.1% of calls is acceptable for a dashboard feed. For a checkout API? That seam blows out.
Error rate and retry overhead
The catch is that error rates during rehashing are rarely flat. They oscillate. One moment you get a cluster-down storm because the proxy hasn't finished gossip; the next, every pipeline chunk hits a MOVED on the same stale slot map. Wrong order—that hurts most. Most teams skip this: tracking error rate per slot during migration. A client that retries aggressively might reduce visible errors to zero while secretly sending three extra round trips for every stalled key. That overhead doesn't show in your error dashboard. It shows in throughput drop. I fixed this once by logging the ratio of retried-to-first-try operations during a rebalance window; the retry-based client looked clean but was actually spending 40% of its time re-sending already-partial pipeline fragments. The right criterion: measure error rate and retry amplification factor side by side. If retries exceed 1.2× normal operations, your strategy is masking latency, not solving it.
'A pipeline that never returns an error is not necessarily a pipeline that returned fast. It only means you paid the cost somewhere else.'
— production engineer, after a three-hour Redis rehash incident
Operational complexity and consistency guarantees
Honestly—the easiest strategy to implement is often the hardest to trust. A client-side retry loop takes two dozen lines of code. A proxy like Redis Enterprise or Twemproxy abstracts the slot logic but introduces a new failure domain: what happens when the proxy itself restarts mid-rehash? I watched a team spend three weeks debugging phantom read-after-write inconsistencies because their proxy forwarded a write to the new shard while the old shard still served stale reads to a second client. That failure mode is invisible in latency and error metrics. The criterion here is not 'how fast', but 'how predictably does the system degrade under partial failure?' Consistency guarantees matter more when the migration hits 60% completion—half your keys on the new slots, half on the old, and the pipeline has no idea which is which. Proxy strategies often enforce read-your-writes at the cost of a synchronous barrier. Client strategies let you tune consistency per key, but you pay for that flexibility in debugging time when the seam finally rips. Pick the criterion that matches your operational tolerance, not just your ambition.
Trade-offs at a Glance: Retry vs Proxy vs Client
Retry: simplicity vs added latency
Picture this: a pipeline issuing 10,000 writes per second suddenly hits a MOVE redirection. A retry loop catches it, re-routes the command, and life moves on. Most teams start here—it's the path of least resistance. The code is trivial: catch the MOVED error, re-send to the correct node, and hope the cluster stabilizes. That sounds fine until the rehashing window stretches past a few seconds. Every retry doubles your worst-case latency; a single stalled key can balloon a 2ms pipeline into a 40ms pause. I have seen this pattern in production—an otherwise healthy cluster that, under rehashing, starts dropping P99 latency from 5ms to 120ms because retries stack on top of retries. The real trap, however, is that retry logic rarely respects backoff. If the rehash is slow, you're essentially fire-hosing the cluster with duplicate work.
The catch is operational simplicity—you own the change entirely inside your application. There is no intermediate layer to debug, no proxy to upgrade. But here is the trade-off: simplicity trades off against tail latency predictability. Retry-based clients can't distinguish between a transient MOVE (quick slot handoff) and a prolonged rehash (ten seconds of slot migration). Both look identical to the client. Wrong order. That hurts.
“The retry approach treats cluster events as rare exceptions. When rehashing becomes routine—and in large clusters it often does—that assumption breaks.”
— production engineer reflecting on a 3-hour incident
Proxy: centralized control vs single point of failure
A proxy layer—something like Twemproxy or Redis Enterprise’s internal router—sits between your application and the cluster. From the client’s perspective, there is no rehashing; the proxy absorbs MOVED responses, buffers the pipeline, and retries internally. This is seductive: your application code never needs to know about slot maps or hash tags. Most teams skip this? Not exactly—they evaluate it, then balk at the operational overhead. A proxy is another stateful service to monitor, scale, and patch. It introduces network hop latency, typically 0.5–2ms per command, which for multi-key pipelines can compound quickly. The real danger: if the proxy goes down, all your Redis traffic halts. That's a single point of failure with a wide blast radius. We fixed this by deploying proxy pairs with VIP failover, but that meant doubling our infrastructure cost. The rehashing isolation came at a price—one that startup teams rarely budget for.
What usually breaks first is the proxy’s connection pool. Under heavy pipeline load, a rehash event causes the proxy to queue outbound requests while it re-resolves slot owners. If the queue grows faster than the proxy can drain it, you hit memory limits or—worse—connection timeouts cascade to clients. Proxy-based strategies centralize control but also centralize risk. That said, for teams with dedicated ops staff and a tolerance for extra hardware, this approach delivers consistent pipeline latency during slot migration. The question is: are you ready to own that extra layer?
Client-aware: direct routing vs version lock-in
Client-aware routing (e.g., using hash tags to guarantee all keys for a pipeline land on the same slot) side-steps rehashing stalls entirely—as long as the slot doesn't move mid-operation. The trick is to design your key schema so that pipeline batches align with slot boundaries. I once worked on a system that stored user session data as user:{id}:session—a perfect hash tag candidate. Pipelines for a single user never crossed nodes. But when we expanded to cross-user analytics pipelines, the schema broke. Retrofitting hash tags into an existing key space is painful: you rewrite query patterns, update cache invalidation logic, and retrain the team. The trade-off is architectural purity versus version lock-in. Once your application hard-codes slot awareness, you're coupled to Redis Cluster’s specific hashing algorithm. Switching to a different Redis deployment model—or a different key-value store—means rewriting that routing logic. Not impossible, but expensive.
Field note: redis plans crack at handoff.
Field note: redis plans crack at handoff.
Most teams underestimate the cognitive load. Developers must think in terms of 16,384 slots while writing business logic. One misaligned key and your pipeline scatters across nodes, defeating the whole purpose. However—and this is the critical nuance—client-aware routing offers the lowest tail latency during rehashing. No retry waits, no proxy queues. Your pipeline runs as long as you can guarantee slot stability for the duration of the operation. That's a big if. In practice, we achieved this by batching only within a 50-millisecond window. Outside that? We fell back to retries. Hybrid patterns often win, but they complicate the codebase. Choose client-aware only if your key model is young and your team is willing to live inside the slot map.
Implementing Your Chosen Strategy Without Breaking Production
Step-by-step for client-side retry with backoff
Start by identifying which commands in your pipeline actually choke during rehashing. Most teams skip this—they blanket-retry everything. Wrong order. A slot migration produces MOVED or ASK redirections; your retry logic must catch those specific errors, not timeout exceptions from an overloaded node. I have seen a production incident where retrying *all* failures turned a two-second stall into a forty-second cascade. Ouch.
The implementation itself is deceptively simple: wrap your pipeline loop in a retry layer with exponential backoff capped at 500ms. Pseudocode: max_retries=3, base_delay=50ms, jitter ±20%. But the pitfall hides in the cluster topology refresh. If your client caches stale slot mappings, retries just hit the same wrong node—waste of cycles. Force a CLUSTER NODES refresh after the first redirect. One hard rule: never retry in the same connection context where the redirect arrived. Open a fresh connection to the target node; otherwise you risk pipeline ordering corruption.
The catch is consistency. Retries don't fix rehashing stalls—they pause them. If your pipeline is atomic (multi-key ops), partial retries can leave half a batch applied. I have debugged exactly that: six keys moved, three applied, consumer got a splintered result. Consider making retries idempotent or aborting the entire batch on the second redirect.
Setting up a proxy (Twemproxy example)
Twemproxy (nutcracker) hides rehashing from clients entirely—but only if you configure it correctly. Default settings will drop your pipeline during a reshard. The key: redis: true and preconnect: true. Without preconnect, the proxy creates connections lazily, and a slot migration can strand a pipeline request mid-handshake. That hurts.
Sample config snippet for a 4-node cluster:
my_cluster: listen: 127.0.0.1:22121 hash: fnv1a_64 distribution: ketama auto_eject_hosts: true redis: true preconnect: true server_retry_timeout: 2000 servers: - 10.0.1.1:6379:1 node1 - 10.0.1.2:6379:1 node2 - 10.0.1.3:6379:1 node3 - 10.0.1.4:6379:1 node4The trade-off surfaces immediately: auto_eject_hosts. When a node stalls during rehashing, Twemproxy ejects it after server_failure_limit (default 2). That means your pipeline sees a sudden gap—requests to ejected slots fail fast with DISPATCH_ERR. Honestly—you might be swapping one stall for another. I have seen teams set server_failure_limit to 100 to outlast a rehash, but that just masks a dead node behind hanging connections. What usually breaks first is the proxy's connection pool: rehashing opens and closes sockets rapidly, and Twemproxy's single-threaded event loop chokes under 10K connection churn. Monitor curr_connections; if it spikes above 2x baseline, you need to tune timeout (500ms) to drain stale conns faster.
'The proxy made our 99th percentile response disappear—then reappear as connection timeouts two hours later.'
— lead at a fintech shop, after their 12-node cluster reshards during trading hours
Configuring cluster-aware clients (Lettuce) for rehashing
Lettuce handles slot migrations natively, but its defaults are optimistic. The first thing to change: ClusterClientOptions.builder().topologyRefreshOptions(). Default refresh is every 30 seconds—during a rehash that's an eternity. Bring it down to 3 seconds, but only after the first migration event. How? Wire a ClusterTopologyRefreshTrigger that activates on any MOVED response. Most teams skip this: they set aggressive periodic refresh and hammer the cluster with topology requests even during steady state. Wasteful.
The real lever is adaptiveTopologyRefresh (Lettuce 6.x+). Enable it with adaptiveRefreshTriggersTimeout=30s. This keeps the client lazy until a redirect surfaces—then it consumes itself in refresh cycles until the slot settles. Example:
ClusterClientOptions options = ClusterClientOptions.builder() .topologyRefreshOptions(TopologyRefreshOptions.builder() .enablePeriodicRefresh(false) .enableAdaptiveRefresh(true) .adaptiveRefreshTriggersTimeout(30, TimeUnit.SECONDS) .build()) .timeoutOptions(TimeoutOptions.builder() .fixedTimeout(Duration.ofMillis(500)) .build()) .build();But here's the pitfall: adaptive refresh can cascade. If your pipeline hits a MOVED, the refresh triggers, which pauses pipeline dispatch for 100-200ms—long enough for other parallel pipelines to trip their own timeouts, spawning more moves. A cascade. I have fixed this by adding a debezium-style circuit breaker: if more than 10% of commands redirect within a 1-second window, suppress further refreshes for 5 seconds. Let the proxy layer or the server-side migration coordinator catch up.
Flag this for redis: shortcuts cost a day.
Flag this for redis: shortcuts cost a day.
One more config detail: socketOptions.keepAlive() to true. Rehashing can kill idle TCP sessions; Lettuce keeps them alive but doesn't retransmit quickly enough. With keepalive, an idle pipeline doesn't get silently dropped—it gets a quick RST and triggers a clean retry rather than a 30-second timeout. That alone cut our stall incidents by half in a staging run. Not bad for a single flag.
Risks of Getting Rehashing Wrong
Split-brain scenarios during migration
The quietest disaster in Redis Cluster rehashing is a split-brain that nobody notices until the pager goes off at 3 AM. Here is how it happens: you migrate slot 5432 from node A to node B, but your client library still holds a stale routing table—so writes land on A while the cluster thinks B owns the slot. Both nodes accept writes. Neither protests. By morning you have two diverging histories for the same keys, and reconciliation? Not built into the protocol. I have debugged exactly this outage at a fintech shop where payment retry counters lived in that slot. The counters told conflicting truths. Wrong order. That hurts.
The real kicker is that Redis Cluster's gossip protocol eventually heals the routing view—but eventual means minutes. In those minutes your pipeline keeps firing. Each successful write to the wrong node deepens the divergence. Most teams skip this: they assume the cluster's ASK redirect or MOVED error will protect them. It doesn't protect against a client that treats redirects as optional hints. The only defense is to validate slot ownership before the write, not reactively. Even then, a brief window exists during migration where both the old and new node accept writes for the same slot. That window is measured in milliseconds. A stalled pipeline widens it into seconds.
Data loss from partial writes
Rehashing doesn't move keys atomically. The cluster migrates a batch of keys, pauses, migrates the next batch. If your pipeline is mid-flight when the slot lock engages, some writes succeed and some return errors. The partial write is invisible from the client's perspective—unless you check every response code. Few pipelines do. We lost about 12% of the writes in that slot during the migration window. The application only noticed because the dashboard looked wrong.
— SRE lead at a CDN provider, internal postmortem
That 12% loss didn't surface as a crash. It surfaced as stale recommendations and missing user preferences. The team had chosen a simple retry-on-error strategy without tracking which operations actually completed. Their retries piled onto already congested nodes. The partial write problem compounds when you batch multi-key operations: a single pipeline call might update three keys, but only two survive the rehash. Your application reads back a broken invariant. Unpredictable. Hard to test because staging clusters rarely run rehashing load at production scale. We fixed this by inserting a sequence-id into every pipeline write and verifying the count on the reply side—costly, yes, but it caught the three-out-of-four scenario the first week.
Cascading failures from retry storms
The retry-on-error strategy looks safe on paper. In practice it's the most common path to a cascading failure I have seen in Redis rehashing incidents. Here is the chain: rehashing triggers, your pipeline stalls on a handful of slots, the client retries those writes, each retry consumes a connection from the pool, other pipelines queue behind them, latency climbs, more operations time out and retry. Suddenly your 500-node cluster is handling 8,000 retries per second for 50 keys that can't move fast enough. The cluster responds by slowing down—more MOVED errors, more timeouts. The retry storm becomes self-sustaining.
The catch is that most retry libraries use exponential backoff capped at 30 seconds. That cap ensures the retries never go away entirely during a long rehash. They just cluster in waves. I watched a gaming backend burn 40% of its Redis capacity on retries for a single slot that took 11 seconds to migrate. The team behind it had tested retry logic against a single-node instance. They had never seen what happens when 120 concurrent pipelines all decide to retry the same stalled write. The proxy approach avoids this by queuing writes at the middleware layer, but proxies introduce their own failure domain—if the proxy box dies during rehashing, you lose the write buffer. There is no free lunch. The question is which risk profile fits your traffic pattern and tolerance for partial data loss.
Mini-FAQ: Rehashing and Pipeline Stalls
Can rehashing be avoided entirely?
Not if you expect a cluster to grow. You could pre-split into 512 shards on day one—a team I consulted tried exactly that. They added no slots for eighteen months. Then a new data center came online and they still had to redistribute. The seam blew out during a Black Friday burst. Avoidance is a deferral, not a solution. The real question is whether you can schedule rehashing into a maintenance window. If your SLA allows five minutes of degraded throughput, you can drain slots in batches and keep pipelines alive. Most orgs can't get that window. They need to survive rehashing mid-traffic.
That sounds fine until you realize Redis Cluster's rehashing isn't atomic. One node moves slot 1024, another migrates slot 1025, and your pipeline straddles both. Errors come back as MOVED or ASK redirections. Your client either handles those transparently or stalls while the stack unwinds. I have seen a 120-node cluster drop 40% of its pipeline throughput during an unoptimized reshard. The stall wasn't the data movement—it was the accumulated retry backoff.
Does pipelining make stalls worse?
Yes—and this surprises people. A non-pipelined client sends one command, waits for the reply, then sends the next. If a slot migrates mid-request, only that single command needs retrying. A pipeline batches thirty commands into one TCP write. If commands 12 through 18 hit migrated slots, the entire batch must either be resent or partially re-executed. The catch is that most client libraries either retry the whole pipeline or drop the partial results. Wrong order. You want the client to track which commands succeeded, re-issue only the failed ones, and reassemble the reply order. Few clients do this correctly out of the box. JedisCluster? It retries the whole batch. Lettuce? Slightly better—it can split the pipeline, but only if you configure it. Most teams skip this config. They blame Redis. The real culprit is the client's retry granularity.
'We saw pipeline latency jump from 2ms to 380ms during reshard. The client was re-sending thirty commands every time one slot moved.'
— SRE lead, fintech platform with 2k ops/sec per node
Should I use a proxy for rehashing?
Proxies like Redis Sentinel or Envoy's Redis filter abstract slot topology away from the client. During rehashing, the proxy intercepts MOVED errors and either retries the command against the new node or queues it until the slot migration finishes. That sounds like free robustness. The pitfall: the proxy becomes a single choke point for your entire pipeline. We fixed this once by adding a second proxy layer—only to watch both proxies fight over slot ownership tables. The retry logic doubled latency. Worse, the proxy's internal buffer filled because pipelines arrive faster than the proxy can retry. Backpressure from the proxy stalled the application threads. What usually breaks first is not the Redis cluster but the proxy's memory ceiling. If you go proxy, run at least two instances with a consistent routing hash, and test with a rehashing simulation that moves 10% of slots in thirty seconds. Without that test, you're flying blind.
One last thing—don't assume the proxy handles all redirections. Some implementations only forward MOVED but ignore ASK responses. That partial coverage creates a failure mode where half your pipeline works and half stalls silently. I have debugged that at 3 AM. Not recommended.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!