skip to content

How would you implement a Redis-based dedup store with SETNX, and what TTL/cleanup and durability concerns must you handle?

level: seniorimportance: should knowfreq 40%

answer

  1. SET key 1 NX EX <ttl> (atomic, not SETNX+EXPIRE)
  2. OK => first time; nil => duplicate
  3. TTL = cleanup; size > max duplicate window
  4. in-memory => crash loses recent keys (AOF/RDB)
  5. maxmemory eviction can silently drop dedup keys

basics

~20 s

Use SET key value NX EX <ttl>: it sets the key only if absent (NX) and auto-expires after the TTL (EX). If the command returns 'not set', you've seen the message — skip it. TTL bounds memory; pick it longer than any plausible duplicate window.

solid answer

~60 s

Redis gives an atomic check-and-set via `SET key val NX EX <seconds>`: NX means set-only-if-absent, EX sets a TTL. The atomicity is the point — two concurrent consumers can't both 'win' the same key. If the SET returns OK, you're the first to see this message and proceed; if it returns nil, it's a duplicate, skip. TTL handles cleanup automatically (no sweeper job needed) and bounds memory; size it longer than your worst-case duplicate window — consumer lag, retry backoff, and DLQ replay horizons. Key durability caveats: Redis is in-memory; with default async persistence (RDB/AOF every-second) a crash can lose the most recent keys, re-admitting duplicates — acceptable only if your side effect tolerates rare repeats. Also, SETNX-then-side-effect isn't atomic across Redis and the external effect, so a crash after SETNX but before the effect can mark a message 'done' that never happened (lost work). For strong safety, prefer co-locating the dedup key with a transactional side-effect store; use Redis dedup when speed matters and rare gaps are tolerable.

go deeper

for a junior

Know SET key NX EX sets a key only if it's new and gives it an expiry; nil means you've seen it.

for a middle

Use the atomic SET NX EX, justify the TTL against the duplicate window, and namespace keys.

for a senior

Reason about Redis durability gaps, cross-system non-atomicity, and eviction policy pitfalls.

for a principal

Decide Redis-vs-transactional-DB dedup based on durability needs, throughput, and acceptable duplicate tolerance, and set org defaults.

## The Redis primitive The modern, correct command is a single atomic call: ``` SET <dedupKey> 1 NX EX 86400 ``` - **NX** — *Not eXists*: perform the set only if the key is absent. Returns the value/OK on success, `nil` if the key already existed. - **EX 86400** — set a **TTL** of 86400 seconds (1 day); Redis auto-deletes the key when it expires. (The legacy `SETNX` command does the NX part but can't set a TTL atomically — combining `SETNX` then `EXPIRE` has a race if you crash between them. Always use `SET ... NX EX`.) Decision logic: - **OK returned** → first time seeing this key → process the message. - **nil returned** → duplicate → skip. The atomic NX is what makes this safe under **concurrency**: if two consumer threads/instances race on the same key, exactly one gets OK. ## TTL and cleanup The TTL is your cleanup mechanism — no separate sweeper needed (unlike a SQL table, which needs a scheduled DELETE). Sizing: - **Too short**: an old duplicate arriving after expiry is re-admitted and re-processed. The TTL must exceed the **maximum realistic duplicate window**: consumer lag spikes, retry/backoff durations, manual replays, and dead-letter-queue reprocessing horizons. - **Too long**: more keys held in memory → higher RAM cost. Redis keys are small but billions of them aren't free; estimate `peak_msg_rate * TTL` for worst-case key count. ## Durability concerns (the Redis-specific risk) Redis is **in-memory** with optional persistence: - **RDB snapshots** — periodic; a crash loses everything since the last snapshot. - **AOF** — append-only log; with the common `appendfsync everysec` setting, up to ~1 second of writes can be lost on crash. Either way, a crash can **lose recently written dedup keys**, re-admitting their messages as 'new' → duplicate side effects. This is acceptable only if your side effect tolerates rare duplicates. For stricter needs use AOF `appendfsync always` (slower) or, better, don't rely on Redis alone. ## The cross-system atomicity gap `SET NX` and the side effect are in **different systems** (Redis + your effect). Crash windows: - Crash after `SET NX` OK but before the side effect → key marked done, effect never happened → **lost work** on redelivery (the dedup skips it). - To mitigate: write an 'in-progress' marker first and only finalize after the effect, with a reconciliation/timeout to recover orphaned markers — more complex and still imperfect. This is why, when the side effect is a DB write, a **DB UNIQUE constraint co-committed with the effect** is strictly safer than Redis dedup. Choose Redis when throughput/latency dominate and rare gaps are acceptable, or when the side effect itself lives in Redis. ## Operational notes - **Key naming**: namespace it, e.g. `dedup:orders:{orderId}`, to avoid collisions across consumers. - **Clustering**: in Redis Cluster, a single key maps to one slot — fine; no cross-slot transaction needed for one key. - **Memory eviction**: ensure the dedup keyspace isn't subject to an LRU/LFU `maxmemory-policy` that could evict non-expired keys, silently re-admitting duplicates. Use a policy like `noeviction` or isolate dedup data.

  • Why prefer SET ... NX EX over SETNX followed by EXPIRE?
    SETNX then EXPIRE is two commands; a crash between them leaves a key with no TTL, which never expires and leaks memory forever. SET ... NX EX is a single atomic command that always sets the TTL.
  • What happens if Redis evicts a dedup key before its TTL due to maxmemory pressure?
    The key disappears, so a later duplicate of that message is treated as new and re-processed. Use a noeviction policy or isolate dedup keys so the eviction policy can't drop them prematurely.

saying these in an interview costs you the question

  • Using SETNX then a separate EXPIRE (race leaves keys with no TTL).
  • Assuming Redis dedup is durable across crashes (default persistence can lose recent keys).
  • Treating SET NX + remote side effect as atomic — it isn't across systems.
  • Ignoring maxmemory eviction policy silently dropping dedup keys before TTL.

context