notes~/ notes/redis-hot-writes
~/ cat notes/redis-hot-writes.md

One viral post, one very unhappy shard.

A single counter maps to one hash slot and therefore one primary — so adding Redis shards does nothing for it. The fix is to separate per-entity skew from system-wide capacity, and to recognise those as two different knobs that get turned for two different reasons.

16,384 fixed hash slots1 key = 1 slot = 1 primaryN fixes skewM fixes capacity

00The core idea

the sentence to lead with

“Separate per-entity skew from system-wide capacity. A viral post should change application-level bucketing. Overall traffic growth should change infrastructure — Redis primaries, and when planned, Kafka partitions. Turning the infrastructure knob to fix a skew problem does not work.”

post:101:likes  →  one key  →  one hash slot  →  one primary  →  HOT SHARD

no amount of adding primaries changes that chain

01Logical bucketing

one logical counterpost:101:likesCRC16 % 16384 → one slotONE primary takes every write100 shards does not helpN logical bucketspost:101:likes:0post:101:likes:1post:101:likes:2post:101:likes:3… up to N−1N independently hashable keys → many primariesbucket = stable_hash(user_id) % Napplication hashing spreads one hot entity; Redis Cluster hashing maps keys to capacity. Different problems, different layers.
Adding shards never splits one key. Only changing the key does.

Split one logical counter into many independently hashable keys, and route each user deterministically: bucket = stable_hash(user_id) % N. The single-key bottleneck disappears because there is no longer a single key.

The cost is that a read now needs an aggregation step — either summing buckets at read time, or maintaining a periodically recomputed total so the read path stays one GET.

02Two hashing layers, two jobs

application routing
  user_id  →  stable_hash(user_id) % N  →  logical bucket

Redis routing
  post_id + bucket  →  CRC16(key) % 16384  →  hash slot  →  physical primary
LayerProblem it solvesWho controls it
Application hashingSpreads one hot logical entity across many keysYou, in application code
Redis Cluster hashingMaps those keys onto physical capacityRedis, automatically

They are not alternatives and neither substitutes for the other. Redis will happily balance slots perfectly and still leave you with one scorching key, because balancing slots is not the same as balancing load.

03Redis Cluster, slots and resharding

A cluster has exactly 16,384 hash slots, assigned to primaries — not necessarily contiguously. A shard is a primary plus replicas; replicas give availability and optional read scaling, and add no primary write capacity.

The client is cluster-aware: it discovers topology, caches the slot-to-primary map, computes the slot for each key and talks to the right primary directly. It is not a coordinator — every app process has its own. It also handles ASK (this key has moved for now, during migration) and MOVED (slot ownership changed permanently — refresh your map).

slots are not consistent hashing

Redis Cluster uses a fixed intermediate layer of 16,384 slots rather than a hash ring with virtual nodes. Both avoid remapping everything when nodes change; Redis picks explicit slots because they are easy to rebalance, migrate and describe in cluster metadata. Build a ring in a custom cache if you like — but do not layer one on top of a datastore that already has its own partitioning model.

resharding 10 primaries → 11

  before: 10 primaries ≈ 1,638 slots each
  after:  11 primaries ≈ 1,489 slots each

a balanced rebalance takes slots from several primaries, not one.
application keys never change — only which primary owns their slots.

04How many buckets?

Logical bucket count N and physical shard count M are independent. With 64 buckets and 16 shards the average is 4 buckets per shard — but the actual placement is probabilistic. A shard may get 7, or 2, or none for that particular post. Redis balances slots, not “buckets per post”.

skew math, for interviews

  buckets landing on one shard ≈ Binomial(N, 1/M)

  mean   μ = N / M
  stddev σ = sqrt(N · (1/M) · (1 − 1/M))

  typical hottest shard ≈ μ + sqrt(2 ln M) · σ
  for M ≈ 10, sqrt(2 ln M) ≈ 2.15

then convert back to QPS:  per-bucket QPS = total QPS / N
never capacity-plan from total QPS / shard count alone
At 1M writes/secPer bucket
64 buckets15,625 QPS
256 buckets3,906 QPS
1024 buckets~977 QPS

Use substantially more logical buckets than physical shards, so placement luck stops mattering — but not absurdly more, because every bucket adds keys, metadata, aggregation work and operational surface.

05Three knobs: N, M and B

N — logical bucketsper hot entityfixes: one entity is hotM — Redis primariescluster-widefixes: total capacityB — events per writebatched via Kafkafixes: command rate + costone post goes viralthe whole cluster is hottoo many Redis opswhy raising M alone fails at 10M QPS10,000,000 ÷ 64 buckets = 156,250 QPS on a single bucket keyone primary handles ≈150k — so that one key is already over capacitygoing from 10 to 100 primaries does not split it. One key, one slot, one primary.raise N first: 10,000,000 ÷ 1024 ≈ 9,766 QPS per bucket — now M can absorb it
Three knobs, three different problems. Coupling them is the usual mistake.

This is the heart of the topic. At 10M QPS with only 64 buckets, one bucket carries ~156K QPS — already past what a primary comfortably serves. Scaling from 10 to 100 primaries changes nothing for that key, because one key still maps to one slot on one primary. N has to rise first.

And the corollary worth stating: you can scale 1M → 10M without batching at all, given enough buckets and enough primaries. Batching is a cost and efficiency optimisation, not the only survival route.

06Kafka batching, done safely

Ten million like events per second need not become ten million Redis commands. Consumers can aggregate increments for the same (post_id, bucket) and issue one INCRBY key 10000 instead of ten thousand INCRs.

the crash window this opens

Redis INCRBY succeeds, the consumer crashes, the Kafka offset was never committed, the records are redelivered — and the batch is counted twice. In-memory batching is only safe because Kafka still holds the original events; the protection has to be explicit.

atomic, in one Lua script:

  if incoming_offset already applied:
      return DUPLICATE
  INCRBY counter delta
  update last_processed_offset

then, and only then, commit the Kafka offset

A high-watermark offset is only safe if that partition is processed sequentially — otherwise a later offset can complete first and make an earlier one look like a duplicate.

the general lesson

First ask whether all related changes can happen in one transactional boundary — if yes, take it, because it gives the simplest correctness model. Only when operations span systems that cannot be atomic do you reach for step-level idempotency, conditional updates, outbox/inbox or state machines. Atomicity means all-or-nothing; idempotency means repeating is harmless. Strong systems use both.

07Changing N safely

N can be raised dynamically from per-post metrics — write QPS, queue lag, latency contribution. The catch is that changing N changes the modulo mapping:

old: bucket = stable_hash(user_id) % 64
new: bucket = stable_hash(user_id) % 256

the same user now maps somewhere else — so the cutover needs
explicit semantics, usually versioned routing:

  post:101 metadata
    mode         = SHARDED
    version      = 3
    bucket_count = 256

  new writes → post:101:v3:likes:[bucket]
  total      = sum(v1) + sum(v2) + sum(v3)

Promotion works the same way: a normal post is one key, and when it crosses a threshold the existing count is frozen as a base while new likes go to buckets — total = base + sum(buckets). At very large scale it is often simpler to use stable bucketing from the start than to operate a promotion path.

08What is automatic, what you decide

AutomaticYour design decisionPlatform / SRE
Kafka record → partition routingChoosing the Kafka keyProvisioning brokers and Redis nodes
Key → slot calculationBucket strategy and countChanging Kafka partition count
Slot → primary routingHot-entity thresholdsAdding primaries and replicas
ASK / MOVED handlingKey layout and hash tagsTriggering rebalance / resharding
Replication and failoverBatching window, idempotency, timeoutsCluster health, rollout and rollback
do not raise Kafka partitions for one viral post

Partition count is an infrastructure decision sized from expected throughput and consumer parallelism — a normal post and a viral post share the same pool. Increasing it is infrequent, changes future keyed routing, and leaves historical records where they were. Start with a large stable pool and use a routing key that lets one hot entity spread across it.

09Question drill

One counter is overloading a shard. Add more shards?

No. That key hashes to one slot owned by one primary, and more primaries does not change that. Split the logical counter into N bucket keys so there are N independently hashable keys to distribute.

What are the two hashing layers and why both?

Application hashing (hash(user_id) % N) spreads one hot entity across many keys. Redis Cluster hashing (CRC16 % 16384) maps keys onto physical capacity. The first solves skew, the second solves capacity — neither substitutes for the other.

How many buckets should a hot post have?

Substantially more than the physical shard count, so random placement stops mattering. Work it from QPS: at 1M writes/sec, 64 buckets is 15,625 QPS each; 1024 buckets is ~977 each. Then check the skew — the hottest shard is roughly μ + 2.15σ for around 10 shards.

Can you go from 1M to 10M QPS without batching?

Yes, if N is large enough and Redis has time and headroom to scale M. Batching reduces the number of Redis operations and therefore cost — it is an efficiency lever, not the only way to survive growth.

At 10M QPS with 64 buckets, why won't 100 primaries save you?

Because 10,000,000 ÷ 64 = 156,250 QPS on a single bucket key, and one primary handles around 150k. That key exceeds one primary's capacity, and a key cannot be split across primaries. Raise N first.

Does Redis Cluster use consistent hashing?

Not a classic ring. It uses a fixed layer of 16,384 slots mapped to primaries. Same goal — avoid remapping everything when nodes change — but explicit slots are easier to rebalance, migrate and describe in metadata.

What is the difference between ASK and MOVED?

ASK is temporary: during a slot migration this particular key now lives on another node, retry there. MOVED is permanent: slot ownership changed, so update your cached topology.

Do replicas add write capacity?

No. Replicas provide availability and can serve reads. Writes go to the primary owning the slot, so replicas do nothing for a write-hot key.

A viral post appears. Which knob do you turn?

N — application-level bucket count for that entity. M only moves when the whole cluster shows sustained pressure: CPU, p99, network, memory, command rate, failover headroom. And Kafka partitions are not a per-post lever at all.

Consumer batches increments, crashes before committing. What now?

Kafka redelivers and the batch would be applied twice. Make the dedupe check, the INCRBY and the offset update one atomic Lua script, then commit the Kafka offset. A high-watermark offset requires sequential processing within the partition.

How do you change N on a live hot post?

Versioned routing. Keep mode, bucket_count and version in metadata, send new writes to v3 keys, and compute the total across versions. Changing the modulo without versioning silently remaps users mid-flight.

10Cheat sheet

TermOne line
N — logical bucketsPer-entity skew control. Raised when one entity gets hot.
M — primary shardsTotal cluster capacity. Raised when the whole cluster is pressured.
B — batch sizeEvents per Redis command. Lowers command rate and cost.
Hash slotOne of 16,384 fixed routing buckets owned by a primary.
Hash tag{...} forcing related keys into one slot so Lua can span them.
ASKTemporary redirect during slot migration.
MOVEDPermanent ownership change — refresh the topology cache.
ReshardingMoving slots (and their keys) between primaries.
PromotionFreezing a normal counter as a base and moving new writes to buckets.
Versioned routingMetadata making a bucket-count change safe mid-flight.

A viral post changes N. A busy cluster changes M. Confusing the two is the whole trap.

← all notes