Redis Cluster vs Sentinel for Deduplication State

Idempotency keys are unusual Redis data: losing one does not degrade performance, it silently removes a correctness guarantee. That changes how you should evaluate the two high-availability topologies, because the usual framing — availability and throughput — misses the question that matters most, which is what happens to acknowledged writes during a failover. This decision guide sits under Redis cache-based deduplication.

Prerequisites. You should have a working atomic registration and a documented degradation policy for when the store is unreachable.


Step 1 — Size the key space

The topology choice usually falls out of arithmetic rather than preference.

# Live keys = peak registrations/sec × TTL seconds.
peak_rps, ttl_hours, bytes_per_key = 2_000, 24, 180
live_keys  = peak_rps * ttl_hours * 3600            # 172,800,000
raw_bytes  = live_keys * bytes_per_key              # ~31 GB
with_overhead = raw_bytes * 1.35                    # ~42 GB — hash + expiry structures
print(f"{live_keys:,} keys, ~{with_overhead / 1e9:.0f} GB")
Live key volume Peak registrations/sec Recommended topology
under 10 GB under 20,000 Single primary + replica, Sentinel
10–30 GB 20,000–50,000 Sentinel, with a plan to shard
over 30 GB over 50,000 Cluster, 3+ shards
any, with strict durability any Cluster plus a relational backstop

That last row is the one teams skip. Neither topology makes an acknowledged write durable, so a system that genuinely cannot tolerate a lost key needs a unique constraint in the transactional database as the authoritative record, with Redis as a fast negative cache in front of it.


Step 2 — Quantify the lost-write window

The asynchronous replication window during failover Two lifelines, primary and replica. The client registers a key and the primary acknowledges immediately. The replication stream carries the key toward the replica but has not arrived when the primary dies. Sentinel or Redis Cluster promotes the replica, which has no record of the key. The client's retry finds an absent key and executes a second time. A shaded band marks the lost-write window, typically one to fifty milliseconds of unreplicated writes. primary replica client: SET NX OK (acknowledged immediately) lost-write window key on primary only 1–50 ms of writes replication stream never arrives primary dies replica promoted client retry: key absent → executes twice

Both topologies replicate asynchronously and both lose the window. The differences are in how quickly failover completes and how many keys are affected:

Property Sentinel Cluster
Failover detection down-after-milliseconds, typically 5 s cluster-node-timeout, typically 15 s
Failover completion 6–12 s 15–30 s
Keys at risk All unreplicated writes on the one primary Unreplicated writes on the failed shard only
Blast radius 100% of the key space 1/N of the key space
Client complexity Sentinel-aware client, single logical endpoint Cluster-aware client, slot map, MOVED handling
Multi-key operations Unrestricted Same-slot only — hash tags mandatory

WAIT 1 100 after each registration blocks until one replica acknowledges or 100 ms elapses. It shrinks the window materially and does not close it, because a replica that acknowledged can itself fail before promotion. Treat it as risk reduction with a latency cost — typically 1–3 ms added at p99 — not as a durability guarantee.

pipe = r.pipeline(transaction=False)
pipe.set(scoped, payload, nx=True, px=ttl_ms)
pipe.execute_command("WAIT", 1, 100)      # best-effort replication barrier
registered, replicas = pipe.execute()
if registered and replicas < 1:
    metrics.inc("idempotency_unreplicated_write_total")   # visible, not silent

Step 3 — Hash tagging on Cluster

Cluster shards by slot, and any multi-key operation — including a Lua script — must stay inside one slot.

def scoped(credential_id: str, route: str, key: str) -> str:
    # The braces delimit the slot-hashed substring. All keys for one operation
    # share a slot, so Lua scripts and MULTI both remain legal.
    return f"idem:v1:{{{credential_id}:{route}:{key}}}"
redis-cli -c CLUSTER KEYSLOT 'idem:v1:{cred_7:POST/v1/charges:01HXYZ}'
redis-cli -c CLUSTER COUNTKEYSINSLOT 5432       # watch for a hot slot

A subtle trap: tagging on the credential alone — idem:v1:{cred_7}:route:key — puts every key for your largest customer on one shard. Include the key itself in the tag so distribution stays even, unless you have a specific reason to co-locate.


Step 4 — Configure eviction correctly

# redis.conf for a deduplication instance
maxmemory 48gb
maxmemory-policy noeviction        # NOT allkeys-lru — see below
appendonly yes
appendfsync everysec
save ""                            # AOF only; RDB snapshots stall on large datasets

allkeys-lru is the default instinct and it is wrong here. Under memory pressure it silently deletes idempotency keys to make room, which removes the guarantee without any signal. noeviction makes the instance return an error on write instead, which your fail-closed path surfaces as a 503 — visible, alertable and correct.

Size maxmemory at roughly 70% of instance RAM. Redis needs headroom for the copy-on-write fork during AOF rewrite, and an OOM kill during a rewrite loses far more than the eviction you were avoiding.


Step 5 — Test a real failover under load

Measuring the real cost of a failover A chart of duplicate executions per second over time. The line sits at zero during steady load, spikes during the failover window, and returns to zero afterwards. Annotations mark the forced failover, the failover duration, and the total duplicate count which is the number to compare between Sentinel and Cluster. dup/s time forced failover recovered shaded area = duplicate executions this integer is the number to compare between Sentinel and Cluster, and to put in the degradation policy document steady state: 0
# Drive steady load with a fixed duplicate ratio, then force a failover mid-run.
k6 run --vus 200 --duration 5m idempotency_load.js &

sleep 120
redis-cli -p 26379 SENTINEL FAILOVER mymaster        # Sentinel
# or, on Cluster:
redis-cli -c -h shard1-replica CLUSTER FAILOVER FORCE

wait
psql -c "SELECT count(*) FROM (
           SELECT idempotency_key FROM charges
            GROUP BY idempotency_key HAVING count(*) > 1) d;"
# This integer is the real, measured cost of a failover for your workload.

Record the number. It belongs in the degradation policy alongside the fail-open/fail-closed decision, because it is the honest answer to “what happens when Redis fails over” — and “nothing” is not that answer.


Failure scenarios and debugging

Failure Scenario Remediation Steps Observability Hooks
maxmemory-policy left at allkeys-lru, so keys are silently evicted under pressure and duplicates appear with no error anywhere Set noeviction and assert the running config at startup. Add capacity or shorten the TTL rather than letting eviction “solve” it. redis_evicted_keys_total — must stay at exactly zero; alert on any increment
Hash tag built from the credential only, so one large tenant’s keys all land on a single shard and it saturates Include the key in the tag so distribution is uniform. Rebalance slots after the fix. redis_cluster_keys_per_slot p99 against the median; a ratio above 5 indicates a hot slot
Failover completes but the client library caches a stale slot map and keeps writing to the demoted node Use a slot-map-aware client with MOVED handling and a bounded topology-refresh interval (5 s is typical). redis_moved_redirections_total; a sustained rate means the client is not refreshing
WAIT used with a large timeout, so every registration blocks up to a second during replica lag Cap the timeout at 100 ms and treat a shortfall as a metric, not an error. Registration latency is on the critical path. idempotency_registration_duration_ms p99; idempotency_unreplicated_write_total
Sentinel quorum set to 1 with two Sentinels, producing split-brain promotions during a network partition Run an odd number of Sentinels — three minimum, across failure domains — with quorum 2. sentinel_masters reporting more than one primary for a name; alert immediately
Blast radius of one failover Two key spaces drawn as bars. Under Sentinel the entire key space sits on one primary, so a failover puts all of it at risk. Under Cluster with four shards, a single shard failover puts only a quarter of the key space at risk. The unreplicated write window is the same in both cases; only its reach differs. Sentinel 100% of keys at risk during failover Cluster ×4 shard 1 — at risk shard 2 — fine shard 3 — fine shard 4 — fine The lost-write window is identical; only how much of the key space it touches differs.

SRE / observability checklist

  1. redis_evicted_keys_total — Must be exactly zero on a deduplication instance. Any increment means the guarantee is being deleted to make room.
  2. redis_master_repl_offset minus slave_repl_offset — Gauge of replication lag in bytes. Alert above 1 MB; it is the size of the lost-write window.
  3. idempotency_unreplicated_write_total — Counter of registrations where WAIT returned zero replicas. A rising rate predicts failover damage.
  4. redis_moved_redirections_total — Cluster only. A sustained rate means the client’s slot map is stale.
  5. redis_memory_used_bytes versus maxmemory — Alert at 80%. With noeviction, hitting the ceiling turns into write errors and a fail-closed endpoint.
  6. idempotency_store_unavailable_total — Counter of the fail-closed path. This is what a failover looks like from the application’s side, and it should correlate exactly with the Redis events.