Calculating Hash Slots with CRC16 in Python
You are staring at a CROSSSLOT Keys in request don't hash to the same slot error, or trying to decide which keys will land together before you deploy, and you need to know exactly which of Redis Cluster's 16,384 slots a key maps to — and therefore which node owns it. Redis computes that mapping with a fixed, deterministic formula, so you can reproduce it offline in pure Python: no round trip, no guessing. This page implements the same CRC16 and hash-tag rules the server uses, verifies them against redis-cli, and then maps a slot back to a physical node so you can co-locate related keys deliberately instead of by accident. The slot model itself is established in Redis Cluster slot allocation basics; here we make it computable.
Prerequisites
- A running Redis 7.x deployment in cluster mode (
cluster-enabled yes) with at least three primaries, reachable viaredis-cli -c. redis-py5.x on Python 3.10+ (pip install "redis>=5,<6"), used only for cross-checking — the slot math below has no third-party dependencies.- Familiarity with Redis hash tags: the
{...}substring that forces keys onto a shared slot. - The bytes of your keys are what get hashed, so decide on one encoding (UTF-8 here) and use it everywhere.
Step-by-Step Implementation
1. Understand the slot formula
Every routable Redis Cluster key resolves to a slot through one rule: HASH_SLOT = CRC16(key) mod 16384. The CRC16 is computed over the key's raw bytes, and the modulo folds the 16-bit result (0–65535) into the 0–16383 slot space. Nothing else — no random salt, no server state — enters the calculation, which is why the same key always maps to the same slot on every client and every node.
# The entire routing contract, in one line of pseudocode:
# HASH_SLOT = CRC16(key_bytes) % 16384
# CRC16 here is the CCITT/XMODEM variant: polynomial 0x1021, init 0x0000.
NUM_SLOTS = 16384
2. Extract the hash tag first
Before hashing, Redis checks for a hash tag. If the key contains a {, and somewhere after it a } with at least one byte in between, then only the substring between the first { and the next } is hashed — the rest of the key is ignored. This is the mechanism that lets user:{42}:profile and user:{42}:cart share a slot. An empty pair {} or a { with no closing brace disables the rule and the whole key is hashed.
def hash_tag(key: bytes) -> bytes:
"""Return the substring Redis actually hashes, applying the hash-tag rule."""
start = key.find(b"{")
if start != -1:
end = key.find(b"}", start + 1)
# Only a non-empty {...} span counts; {} and lone { fall through.
if end != -1 and end != start + 1:
return key[start + 1:end]
return key
3. Implement CRC16-CCITT/XMODEM
Redis uses the XMODEM flavour of CRC16: polynomial 0x1021, initial value 0x0000, no input or output reflection. The bit-by-bit version below is the clearest statement of the algorithm and is exactly what the reference crc16.c encodes.
def crc16(data: bytes) -> int:
"""CRC16-CCITT/XMODEM: poly 0x1021, init 0x0000, no reflection."""
crc = 0x0000
for byte in data:
crc ^= byte << 8 # fold each byte into the high 8 bits
for _ in range(8):
if crc & 0x8000: # test the top bit before shifting
crc = (crc << 1) ^ 0x1021
else:
crc <<= 1
crc &= 0xFFFF # keep it a 16-bit register
return crc
4. Precompute a lookup table for hot paths
The bitwise loop is correct but does eight iterations per byte. Production routers precompute a 256-entry table so each byte costs one XOR and one index. The table is generated from the same polynomial, so the two implementations agree bit-for-bit.
CRC16_TABLE = []
for i in range(256):
crc = i << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) if (crc & 0x8000) else (crc << 1)
crc &= 0xFFFF
CRC16_TABLE.append(crc)
def crc16_fast(data: bytes) -> int:
crc = 0x0000
for byte in data:
crc = ((crc << 8) & 0xFFFF) ^ CRC16_TABLE[((crc >> 8) ^ byte) & 0xFF]
return crc
5. Combine into a slot function
Chain the hash-tag extraction and the checksum, then take the modulo. This single function reproduces CLUSTER KEYSLOT for any key.
def key_slot(key: str) -> int:
"""Reproduce Redis Cluster's HASH_SLOT = CRC16(hash_tag(key)) % 16384."""
return crc16(hash_tag(key.encode("utf-8"))) % NUM_SLOTS
# Worked example — the hash tag collapses both keys onto one slot:
assert key_slot("user:{42}:profile") == 8000
assert key_slot("user:{42}:cart") == 8000
assert key_slot("foo") == 12182 # the value Redis docs cite
6. Verify against redis-cli
Never trust a hand-rolled checksum without confirming it against the server. CLUSTER KEYSLOT returns the authoritative slot for any key string, and it applies the same hash-tag rule — so it is your oracle.
redis-cli CLUSTER KEYSLOT "user:{42}:profile" # -> (integer) 8000
redis-cli CLUSTER KEYSLOT "user:{42}:cart" # -> (integer) 8000
redis-cli CLUSTER KEYSLOT foo # -> (integer) 12182
7. Map the slot to a node
A slot number is only useful once you know which primary owns it. CLUSTER SHARDS (Redis 7.0+) reports each shard's slot ranges and endpoints; the loop below turns a slot into the node that serves it, so you can point a debugger — or a targeted redis-cli -h — at the right host.
import redis # sync client is fine for a one-off topology read
def node_for_slot(client: redis.Redis, slot: int) -> str:
for shard in client.cluster_shards():
ranges = shard["slots"] # flat list: [start, end, start, end, ...]
for i in range(0, len(ranges), 2):
if ranges[i] <= slot <= ranges[i + 1]:
master = next(n for n in shard["nodes"] if n["role"] == "master")
return f"{master['endpoint']}:{master['port']}"
raise LookupError(f"slot {slot} is not currently assigned")
Failure Modes
CROSSSLOT on a multi-key command. An MGET, UNLINK, or Lua call spanning keys that hash to different slots is rejected outright — Redis will not scatter one command across shards. Diagnose by slotting each key and comparing:
redis-cli CLUSTER KEYSLOT "user:42:profile" # no tag -> hashes whole key
redis-cli CLUSTER KEYSLOT "user:42:cart" # different slot -> CROSSSLOT
The fix is to add a shared hash tag so both keys reduce to the same substring: rename them user:{42}:profile and user:{42}:cart. Deliberate co-location is the same trick that makes multi-key explicit invalidation atomic, and it is formalized as a design pattern in key tagging strategies for bulk updates.
Unintended hot slot. Over-tagging funnels far too many keys through one substring, so a single slot and its owning node absorb a disproportionate share of traffic while the rest of the Redis cluster idles. Diagnose by counting keys per slot for a suspected tag:
redis-cli CLUSTER COUNTKEYSINSLOT 8000
If one slot holds orders of magnitude more keys than its neighbours, widen the tag (for example, tag by shard-of-tenant rather than by tenant) so the load spreads.
Encoding mismatch. Hashing str in one service and UTF-16 or Latin-1 bytes in another produces different checksums for the same logical key, silently routing reads and writes to different slots. Diagnose by hashing the exact bytes both services send and comparing against the server:
redis-cli CLUSTER KEYSLOT "café:1" # compare to your Python key_slot("café:1")
Standardize on UTF-8 at every boundary and assert the two agree in a unit test.
Verification
Confirm your Python implementation matches the server across a representative sample of keys — plain keys, tagged keys, and edge cases — before you rely on it for routing decisions.
import redis
client = redis.RedisCluster(host="127.0.0.1", port=7000, decode_responses=True)
for key in ["foo", "user:{42}:profile", "user:{42}:cart", "café:1", "{}:literal"]:
assert key_slot(key) == client.cluster_keyslot(key), key
print("local CRC16 slot math matches the cluster for every sample")
Confirm that two keys you intend to co-locate really share a slot, and that a slot resolves to the node you expect:
redis-cli CLUSTER KEYSLOT "user:{42}:profile" # expect the same integer
redis-cli CLUSTER KEYSLOT "user:{42}:cart" # as this one
redis-cli CLUSTER SHARDS # find the shard owning that slot
If the assertions pass, your offline slot predictions are authoritative and safe to use when planning key layouts ahead of a zero-downtime slot migration.
FAQ
Why 16,384 slots and not 65,536?
The slot count is capped so the Redis cluster bus stays cheap. Every node gossips a bitmap of the slots it owns in each heartbeat; at 16,384 slots that bitmap is 2 KB, whereas 65,536 would be 8 KB per message and multiply gossip bandwidth across every node pair. Redis's author judged that clusters realistically top out around 1,000 nodes, and 16,384 slots divide cleanly across that many primaries while keeping the header small — so the modulo in CRC16(key) mod 16384 is a deliberate protocol constant, not an arbitrary one.
What happens with an empty hash tag like {}?
Nothing special — the rule requires at least one byte between the braces. Because {} has an empty span, Redis ignores the tag and hashes the entire key, braces included. So user:{}:profile hashes the literal string user:{}:profile, and two keys that differ only outside an empty {} will land on different slots. Only a non-empty {...} triggers co-location.
With multiple braces, which one wins?
Only the first non-empty pair. Redis scans for the first {, then the first } after it, and hashes whatever lies between. In {a}{b} the tag is a; the {b} is never consulted. In {}{a} the first {} is empty, so — per the rule — the whole key {}{a} is hashed rather than falling through to a. Reason strictly about the first { and the next }, not about "the last tag".
How do I force two keys onto the same slot?
Give them an identical non-empty hash tag. Wrap the shared identifier in braces — order:{c99}:header and order:{c99}:lines both hash c99 and therefore share a slot and a node, which is what makes a multi-key MGET or transaction over them legal. This is exactly how you keep related invalidation targets on one shard, as shown in Redis Cluster slot allocation basics.
Does redis-py compute the slot for me?
Yes. redis-py ships the same algorithm in redis.crc.key_slot, and the RedisCluster client calls it internally to route every command, applying the hash-tag rule as it goes. You rarely need to slot keys by hand at runtime. Reimplementing CRC16 yourself is for the offline cases the client cannot help with: validating a key-naming scheme in CI, predicting layout before data exists, or explaining a CROSSSLOT error in a log where no client is attached.
Up: Redis Cluster Slot Allocation Basics