Redis Cluster returns -ASK redirects when a hash slot is being migrated between nodes. This is a normal part of cluster rebalancing and requires clients to acknowledge the temporary migration state before retrying commands.
The -ASK redirect is a temporary redirection response that Redis Cluster nodes emit during slot migration operations. Unlike a -MOVED redirect (which is permanent), an -ASK redirect tells the client to retry a single command on a different node without permanently updating its slot-to-node mapping. During cluster rebalancing, when a hash slot is being transferred from a source node to a destination node, the migration process has specific states: **Migrating State (Source Node):** The source node is configured to send the slot elsewhere. When it receives a command for a key that doesn't exist locally, it sends an -ASK redirect to the destination node. This means the key may already be on the destination, or it may not exist at all. **Importing State (Destination Node):** The destination node is configured to accept the incoming slot, but only from clients that first send an ASKING command. This prevents clients from accidentally accessing a partially-migrated slot before the migration completes. The -ASK redirect is crucial because during migration, keys gradually move from source to destination. A client might find a key on the source on one request and on the destination on the next, so permanent routing table updates would be incorrect.
Redis Cluster uses two types of redirects:
-MOVED redirect (permanent): Indicates a slot is permanently assigned to a different node. Update your client's internal routing table and immediately route future commands for that slot to the new node.
-MOVED 8379 127.0.0.1:7002-ASK redirect (temporary): Indicates a slot is currently being migrated. Do NOT update routing tables. Send only the next command with an ASKING prefix to acknowledge the temporary state.
-ASK 8379 127.0.0.1:7002This distinction is critical: treating -ASK as permanent will break commands during migration.
When your client receives an -ASK redirect, it must send the ASKING command before retrying the original command:
Using redis-cli:
# When you get -ASK 8379 127.0.0.1:7002, connect to that node:
redis-cli -h 127.0.0.1 -p 7002
# In the redis-cli:
127.0.0.1:7002> ASKING
OK
127.0.0.1:7002> GET mykey
"value"Using redis-py (Python):
import redis
# Basic manual handling
def handle_ask_redirect(client, slot, host, port, command, *args):
try:
# Retry with ASKING
temp_client = redis.StrictRedis(host=host, port=port)
temp_client.execute_command("ASKING")
result = temp_client.execute_command(command, *args)
temp_client.close()
return result
except Exception as e:
print(f"Error after ASK redirect: {e}")
raise
# Most clients handle this automatically
client = redis.RedisCluster(startup_nodes=[...], skip_full_coverage_check=True)
result = client.get("mykey") # Handles ASK redirects internallyUsing node-redis (Node.js):
import { createCluster } from "redis";
const cluster = createCluster({
rootNodes: [
{ host: "127.0.0.1", port: 7000 },
{ host: "127.0.0.1", port: 7001 },
],
});
// The client automatically handles -ASK redirects
cluster.on("error", (err) => console.log("Cluster error:", err));
await cluster.connect();
const value = await cluster.get("mykey"); // Handles ASK internally
await cluster.disconnect();Using StackExchange.Redis (C#/.NET):
using StackExchange.Redis;
var options = ConfigurationOptions.Parse("localhost:7000,localhost:7001");
options.TieBreaker = "";
IConnectionMultiplexer redis = await ConnectionMultiplexer.ConnectAsync(options);
// The client automatically handles -ASK and -MOVED redirects
var db = redis.GetDatabase();
var value = await db.StringGetAsync("mykey");
redis.Dispose();Key point: Most modern Redis client libraries handle -ASK redirects automatically. Only use manual ASKING if building a custom Redis client.
Different Redis client libraries vary in their -ASK handling capabilities:
Clients with built-in ASK support (recommended):
- redis-py 4.0+
- node-redis 4.0+
- StackExchange.Redis (all versions)
- Jedis (Java)
- Lettuce (Java)
- go-redis (Go)
- redigo (Go)
Clients requiring configuration:
Some older versions require explicit cluster support:
# Python: Ensure using redis-py with cluster support
from rediscluster import RedisCluster
startup_nodes = [{"host": "127.0.0.1", "port": 7000}]
rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True)
value = rc.get("mykey") # Automatically handles ASK// Node.js: Use cluster-aware client
const Redis = require("ioredis");
const cluster = new Redis.Cluster([
{ host: "127.0.0.1", port: 7000 },
{ host: "127.0.0.1", port: 7001 },
]);
cluster.on("error", (err) => {
console.error("Redis Cluster error:", err);
});
const value = await cluster.get("mykey"); // Handles ASK internallyVerify your client version:
# Python
pip show redis-py
# Node.js
npm list redis
# Java (Maven)
mvn dependency:tree | grep jedisUpdate to the latest version if using an old client.
Monitor ongoing migrations to understand when -ASK redirects will occur:
# Connect to any cluster node
redis-cli -h 127.0.0.1 -p 7000
# View cluster info and migration status
127.0.0.1:7000> CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:7
cluster_my_epoch:2
cluster_stats_messages_sent:12345
cluster_stats_messages_received:12340
# View detailed slot assignment
127.0.0.1:7000> CLUSTER SLOTS
1) 1) (integer) 5461
2) (integer) 10922
3) 1) "127.0.0.1"
2) (integer) 7001
4) 1) "127.0.0.1"
2) (integer) 7004
2) 1) (integer) 10923
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 7002
4) 1) "127.0.0.1"
2) (integer) 7005
3) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 7000
4) 1) "127.0.0.1"
2) (integer) 7003
# Check if any slots are migrating
127.0.0.1:7000> CLUSTER NODES
ccb5b74f2e5a7a9f0a0b1c2d3e4f5a6b 127.0.0.1:7000@17000 myself,master - 0 1640000000000 1 connected 0-5460
7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d 127.0.0.1:7001@17001 master - 0 1640000000000 2 connected 5461-10922
2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b 127.0.0.1:7002@17002 master - 0 1640000000000 3 connected 10923-16383 [5461-<-ccb5b74f2e5a7a9f0a0b1c2d3e4f5a6b]The [5461-<-ccb5b74f2e5a7a9f0a0b1c2d3e4f5a6b] notation indicates that slot 5461 is being imported from node ccb5b74f...
# View logs to see when migration completes
redis-cli -h 127.0.0.1 -p 7000 --csv INFO replication
# Monitor real-time cluster events
redis-cli -h 127.0.0.1 -p 7000 MONITORMulti-key commands have special behavior during migration. If keys for a command are spread across slots being migrated, Redis returns a different error:
# Multi-key command with keys on different slots
127.0.0.1:7000> MGET key1 key2 key3
# If some keys are on a migrating slot: -TRYAGAIN
# Redis returns -TRYAGAIN instead of -ASK for multi-key commands
# Client should retry the entire command after a brief delayHandling -TRYAGAIN:
import time
from rediscluster import RedisCluster
startup_nodes = [{"host": "127.0.0.1", "port": 7000}]
rc = RedisCluster(startup_nodes=startup_nodes)
def mget_with_retry(rc, keys, max_retries=5):
for attempt in range(max_retries):
try:
return rc.mget(keys)
except redis.exceptions.TryAgainError:
wait_time = min(2 ** attempt, 10) # Exponential backoff
print(f"Slot migration in progress, retrying in {wait_time}s...")
time.sleep(wait_time)
except Exception as e:
raise
raise RuntimeError("Max retries exceeded for multi-key command")
values = mget_with_retry(rc, ["key1", "key2", "key3"])Best practices for multi-key operations during migration:
- Add retry logic with exponential backoff
- Use timeouts to detect hanging migrations
- Break large MGET/MSET operations into smaller batches
- Consider using pipelining instead of multi-key commands
**Using pipelines to avoid multi-key errors:**python
from redis import Redis
pipe = rc.pipeline()
pipe.get("key1")
pipe.get("key2")
pipe.get("key3")
results = pipe.execute() # Retries each command separately instead of all at once
```
During slot migration, operations may take longer. Configure client timeouts accordingly:
# Python: redis-py
from redis import Redis
# Set socket timeout for handling redirects
redis = Redis(
host="127.0.0.1",
port=7000,
socket_timeout=5, # 5 second socket timeout
socket_connect_timeout=5,
retry_on_timeout=True,
)
# For cluster
from rediscluster import RedisCluster
rc = RedisCluster(
startup_nodes=[{"host": "127.0.0.1", "port": 7000}],
socket_timeout=5,
connection_timeout=5,
skip_full_coverage_check=True,
)// Node.js: ioredis
const Redis = require("ioredis");
const cluster = new Redis.Cluster([
{ host: "127.0.0.1", port: 7000 },
{ host: "127.0.0.1", port: 7001 },
{ host: "127.0.0.1", port: 7002 },
]);
cluster.on("error", (err) => {
console.error("Cluster error:", err);
});
// Set timeouts
cluster.setMaxListeners(100);
// Configure command timeout
cluster.on("error", (err) => {
if (err.code === "TIMEOUT") {
console.log("Command timeout during migration");
}
});// C#/.NET: StackExchange.Redis
using StackExchange.Redis;
var options = ConfigurationOptions.Parse("localhost:7000,localhost:7001");
options.ConnectTimeout = 5000; // 5 second timeout
options.SyncTimeout = 5000; // 5 second sync timeout
options.AbortOnConnectFail = false; // Continue if initial connection fails
IConnectionMultiplexer redis = await ConnectionMultiplexer.ConnectAsync(options);Timeout recommendations during migration:
- Set socket timeout to at least 5 seconds
- Increase for slow networks or large migrations
- Use exponential backoff for retries
- Log timeout events to monitor migration progress
Ensure all nodes can communicate for slot migration:
# Test communication between cluster nodes
# From one node, verify it can reach others
redis-cli -h 127.0.0.1 -p 7000
# Ping all cluster nodes
127.0.0.1:7000> PING
PONG
# Check node connectivity
127.0.0.1:7000> CLUSTER INFO
cluster_state:ok
cluster_nodes_in_cron_updates:6
# Detailed node information
127.0.0.1:7000> CLUSTER NODES
# Shows: [node-id] [ip:port] [flags] [master] [epoch] [slots]
# Connection status indicated by flags: connected, disconnected, fail
# If any nodes show as 'disconnected' or 'fail':
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7001 # Reconnect to specific nodeNetwork diagnostics:
# From the application server, verify connectivity
nc -zv 127.0.0.1 7000
nc -zv 127.0.0.1 7001
nc -zv 127.0.0.1 7002
# Check firewall rules
sudo ufw status
sudo iptables -L -n | grep 700
# Test with redis-cli from different servers
redis-cli -h redis-node-1 -p 7000 CLUSTER INFO
redis-cli -h redis-node-2 -p 7000 CLUSTER INFOCommon causes of communication failures:
- Firewall blocking cluster ports (TCP 7000-7999)
- DNS resolution issues for node hostnames
- Network partitions or packet loss
- NAT/proxy interfering with cluster protocol
- Insufficient bandwidth for slot migration
Add robust retry handling for -ASK responses:
# Python: Comprehensive retry handler
import time
from rediscluster import RedisCluster
from redis.exceptions import (
ClusterError,
TryAgainError,
ResponseError,
)
class RedisWithRetry:
def __init__(self, startup_nodes, max_retries=5):
self.redis = RedisCluster(
startup_nodes=startup_nodes,
skip_full_coverage_check=True,
)
self.max_retries = max_retries
def execute_with_retry(self, func, *args, **kwargs):
"""Execute Redis command with retry on ASK/TRYAGAIN"""
base_delay = 0.1 # Start with 100ms
for attempt in range(self.max_retries):
try:
return func(*args, **kwargs)
except TryAgainError:
# Slot migration in progress
wait_time = min(base_delay * (2 ** attempt), 10)
print(f"Retry {attempt + 1}/{self.max_retries}: Slot migration, waiting {wait_time}s")
time.sleep(wait_time)
except ClusterError as e:
# Connection or cluster error
if "MOVED" in str(e) or "ASK" in str(e):
# Client should have handled this automatically
print(f"Unexpected redirect: {e}")
raise
except Exception as e:
print(f"Error: {e}")
raise
raise RuntimeError(f"Max retries ({self.max_retries}) exceeded")
def get(self, key):
return self.execute_with_retry(self.redis.get, key)
def set(self, key, value):
return self.execute_with_retry(self.redis.set, key, value)
# Usage
rc = RedisWithRetry([{"host": "127.0.0.1", "port": 7000}])
value = rc.get("mykey")// Node.js: Comprehensive retry handler
const Redis = require("ioredis");
class RedisClusterWithRetry {
constructor(nodes, maxRetries = 5) {
this.cluster = new Redis.Cluster(nodes);
this.maxRetries = maxRetries;
}
async executeWithRetry(command, args, attempt = 0) {
try {
return await this.cluster[command](...args);
} catch (error) {
if (attempt >= this.maxRetries) {
throw new Error(
`Max retries (${this.maxRetries}) exceeded: ${error.message}`
);
}
// Determine if we should retry
const shouldRetry =
error.code === "CLUSTERDOWN" ||
error.code === "TRYAGAIN" ||
error.message.includes("slot");
if (shouldRetry) {
const baseDelay = 100; // 100ms
const wait = Math.min(baseDelay * Math.pow(2, attempt), 10000);
console.log(
`Retry ${attempt + 1}/${this.maxRetries}: waiting ${wait}ms`
);
await new Promise((resolve) => setTimeout(resolve, wait));
return this.executeWithRetry(command, args, attempt + 1);
}
throw error;
}
}
async get(key) {
return this.executeWithRetry("get", [key]);
}
async set(key, value) {
return this.executeWithRetry("set", [key, value]);
}
}
// Usage
const rc = new RedisClusterWithRetry([
{ host: "127.0.0.1", port: 7000 },
]);
const value = await rc.get("mykey");Understanding the Redis Cluster migration protocol:
When resizing a Redis Cluster or rebalancing slots, the migration process follows this sequence:
1. CLUSTER SETSLOT <slot> MIGRATING <destination-node> - Source node marks slot as migrating
2. CLUSTER SETSLOT <slot> IMPORTING <source-node> - Destination node marks slot as importing
3. Client requests flow: Source checks if key exists; if not, sends -ASK to destination
4. Client sends ASKING, then retries on destination
5. CLUSTER GETKEYSINSLOT - Move keys from source to destination
6. CLUSTER SETSLOT <slot> NODE <destination-node> - Complete migration
Why ASK instead of updating the routing table:
If clients immediately updated their routing table on -ASK, they might miss keys still being migrated. By requiring the ASKING acknowledgment on every retry, Redis ensures clients only query the destination for keys that are truly there, preventing "key not found" errors during partial migrations.
ASK vs MOVED in detail:
- -ASK (temporary, single command): "Try next command here but remember I'm still the owner"
- -MOVED (permanent, all future commands): "I'm no longer the owner, update your routing"
Performance implications of ASK redirects:
- Each ASK redirect adds ~1-2ms of latency per command
- During migration of a single slot, only 0.006% of slot operations are affected (~1 in 16,384)
- Large clusters experience minimal performance impact because only one slot migrates at a time
- Most modern clients handle redirects transparently with internal connection pools
Debugging migration issues:
# Enable Redis verbose logging
redis-server --loglevel debug
# Monitor cluster topology changes
redis-cli -h 127.0.0.1 -p 7000 CLUSTER MYID
redis-cli -h 127.0.0.1 -p 7000 CLUSTER SLOTS
# Force a slot migration (for testing)
redis-cli -h 127.0.0.1 -p 7000 CLUSTER SETSLOT 0 MIGRATING <destination-node>
redis-cli -h 127.0.0.1 -p 7001 CLUSTER SETSLOT 0 IMPORTING <source-node>
# Check migration status
redis-cli -h 127.0.0.1 -p 7000 CLUSTER NODES | grep -i migratingSafe migration practices:
- Drain connections before starting migration
- Monitor network bandwidth during key transfer
- Use CLUSTER SETSLOT STABLE to pause migrations if problems arise
- Run migrations during low-traffic windows
- Have rollback procedures ready (restore from backup, restore replica promotion)
ERR Unbalanced XREAD list of streams
How to fix "ERR Unbalanced XREAD list" in Redis
ERR syntax error
How to fix "ERR syntax error" in Redis
ConnectionError: Error while reading from socket
ConnectionError: Error while reading from socket in redis-py
ERR unknown command
How to fix ERR unknown command in Redis
Command timed out
How to fix 'Command timed out' in ioredis