Chapter 23 · Application Patterns: Caching, Rate Limiting, Locks, Idempotency, and Hot-Key Control

Idempotency Keys, Deduplication, Request State Machines, and Retry-Safe APIs

Model idempotency as a request state machine with request fingerprints, bounded replay records, duplicate handling, retry horizons, and failure recovery.

Advanced210–300 minutesidempotency state machine, fingerprints, replay, retry safetyRedis Open Source 8.10.1redis-py 8.1.0 where Python is usedDocker + redis-cli + Python stdlibStandalone loopback lab · DB 0AOF everysec + RDB · maxmemory 0/noeviction baselineNamed ACL users · TLS off only on loopbackFree/local-firstLast reviewed: September 6, 2026

Learning outcomes

This lesson turns Idempotency Keys, Deduplication, Request State Machines, and Retry-Safe APIs into an observable AtlasMart workflow with explicit correctness, failure, and production boundaries.

01

Explain the mechanisms and terminology behind Idempotency Keys, Deduplication, Request State Machines, and Retry-Safe APIs.

02

Collect Redis, client, configuration, and workload evidence before drawing operational conclusions.

03

Reproduce the lesson's deliberately incorrect or failure-prone case, diagnose the mechanism, and verify the repair.

04

Relate the design to memory, persistence, replication/Sentinel/Cluster, security, latency, and client behavior where applicable.

05

Apply the pattern to AtlasMart and state clearly what the implementation guarantees and what it does not guarantee.

Lab prerequisite: start the isolated Chapter 23 node

Run the Chapter 23 setup from Lesson 1 once. Before continuing, verify redis_version:8.10.1, authenticated identity atlasmart-app, database 0, AOF state, and that the endpoint is 127.0.0.1:6431. Do not point these failure/concurrency exercises at production.

Shell · verify existing Chapter 23 lab
docker exec -e REDISCLI_AUTH=AtlasMart-Ch23-App-Lab-Only-2026 atlasmart-redis-ch23 redis-cli --user atlasmart-app PINGdocker exec -e REDISCLI_AUTH=AtlasMart-Ch23-Admin-Lab-Only-2026 atlasmart-redis-ch23 redis-cli --user academy-admin INFO serverdocker exec -e REDISCLI_AUTH=AtlasMart-Ch23-App-Lab-Only-2026 atlasmart-redis-ch23 redis-cli --user atlasmart-app ACL WHOAMI
Reproducible Chapter 23 baseline

Redis Open Source 8.10.1 using the pinned redis:8.10.1 image, exposed only at 127.0.0.1:6431. Standalone topology, logical database 0, AOF everysec plus an RDB save rule, maxmemory 0 unless a lesson explicitly changes a setting on this disposable node, default ACL user disabled, named academy-admin and atlasmart-app users, and fixture prefix atlasmart:ch23:*. TLS is intentionally off only because this mandatory lab is loopback-local; production traffic must follow Chapter 22 network/TLS guidance. Python examples target redis==8.1.0. Search/JSON/vector/time-series/probabilistic features are not required.

1. Problem: the client timed out after payment/order creation

A mobile client sends POST /orders, the server performs the side effect, but the HTTP response is lost. The client retries. Without idempotency, a second request can create a second order. Network retries are normal; “the client will only send once” is not a correctness property.

2. An idempotency key identifies one logical operation, not one TCP request

Scope the key by authenticated principal/tenant plus endpoint/operation so unrelated callers cannot collide. Store a canonical request fingerprint and reject reuse of the same idempotency key with different request content. Do not put raw secrets or unlimited request bodies in Redis.

State Meaning Typical duplicate behavior
PROCESSING One attempt owns active processing Return in-progress/try-later semantics; do not run side effect again.
SUCCEEDED Side effect completed and replay result is stored Return the same status/result within the retention window.
FAILED_RETRYABLE Previous attempt failed before a committed effect Policy may permit a controlled retry.
FAILED_FINAL Operation will not be retried Replay stable failure where appropriate.

3. Reserve the idempotency record atomically

The first request creates a PROCESSING record with fingerprint and TTL. A duplicate with the same fingerprint sees the existing state; reuse with a different fingerprint returns a conflict. The script avoids a client-side EXISTS/HSET race.

Lua · reserve request state
local key = KEYS[1]local fingerprint = ARGV[1]local ttl = tonumber(ARGV[2])local exists = redis.call('EXISTS', key)if exists == 0 then  redis.call('HSET', key, 'fingerprint', fingerprint, 'state', 'PROCESSING')  redis.call('EXPIRE', key, ttl)  return {'STARTED'}endlocal stored = redis.call('HGET', key, 'fingerprint')if stored ~= fingerprint then return {'CONFLICT'} endreturn {redis.call('HGET', key, 'state') or 'UNKNOWN'}

4. Complete only the matching PROCESSING operation

The completion script verifies fingerprint and expected state before storing a bounded replay payload. It then extends retention for the retry horizon. If the authoritative order database is committed but Redis completion fails, the application needs reconciliation from that authoritative system; Redis cannot make a cross-database exactly-once transaction.

Lua · transition PROCESSING → SUCCEEDED
local key = KEYS[1]local fingerprint = ARGV[1]local status = ARGV[2]local response = ARGV[3]local ttl = tonumber(ARGV[4])if redis.call('HGET', key, 'fingerprint') ~= fingerprint then return 0 endif redis.call('HGET', key, 'state') ~= 'PROCESSING' then return 0 endredis.call('HSET', key, 'state', 'SUCCEEDED', 'status', status, 'response', response)redis.call('EXPIRE', key, ttl)return 1

5. Reproduce duplicates and a key-reuse conflict

The Python harness shows first admission, duplicate while processing, same-key/different-body rejection, completion, and replay state. The response body is deliberately tiny. Real systems should cap replay size or store a compact pointer to authoritative result state.

Python · deterministic idempotency state transitions
import hashlib, json, redisr=redis.Redis(host="127.0.0.1", port=6431, username="atlasmart-app",              password="AtlasMart-Ch23-App-Lab-Only-2026", decode_responses=True)RESERVE=r.register_script("""local key = KEYS[1]local fingerprint = ARGV[1]local ttl = tonumber(ARGV[2])local exists = redis.call('EXISTS', key)if exists == 0 then  redis.call('HSET', key, 'fingerprint', fingerprint, 'state', 'PROCESSING')  redis.call('EXPIRE', key, ttl)  return {'STARTED'}endlocal stored = redis.call('HGET', key, 'fingerprint')if stored ~= fingerprint then return {'CONFLICT'} endreturn {redis.call('HGET', key, 'state') or 'UNKNOWN'}""")COMPLETE=r.register_script("""local key = KEYS[1]local fingerprint = ARGV[1]local status = ARGV[2]local response = ARGV[3]local ttl = tonumber(ARGV[4])if redis.call('HGET', key, 'fingerprint') ~= fingerprint then return 0 endif redis.call('HGET', key, 'state') ~= 'PROCESSING' then return 0 endredis.call('HSET', key, 'state', 'SUCCEEDED', 'status', status, 'response', response)redis.call('EXPIRE', key, ttl)return 1""")def canonical_fingerprint(body):    payload=json.dumps(body, sort_keys=True, separators=(",", ":")).encode()    return hashlib.sha256(payload).hexdigest()principal="customer-42"; endpoint="POST:/orders"; idem="req-2026-0001"key=f"atlasmart:ch23:idem:{principal}:{endpoint}:{idem}"body={"sku":"sku-42","qty":1}; fp=canonical_fingerprint(body)r.delete(key)print("first", RESERVE(keys=[key], args=[fp, 60]))print("duplicate_while_processing", RESERVE(keys=[key], args=[fp, 60]))print("same_key_different_body", RESERVE(keys=[key], args=[canonical_fingerprint({"sku":"sku-99","qty":1}),60]))print("complete", COMPLETE(keys=[key], args=[fp, 201, '{"order_id":"ord-42"}', 300]))print("duplicate_after_success", RESERVE(keys=[key], args=[fp, 60]))print("stored", r.hgetall(key), "ttl", r.ttl(key))

6. TTL is a business retry horizon, not cleanup trivia

If the idempotency key expires before a legitimate delayed retry, the same logical request can be processed again. Retention must cover client retry behavior, asynchronous queues, payment/provider callbacks, and legal/business requirements. Excessively long retention raises memory/cardinality and privacy cost. Measure active keys and MEMORY USAGE rather than guessing.

7. Recovery from abandoned PROCESSING needs an explicit policy

A worker can crash after reserving PROCESSING. Do not simply let every duplicate take over immediately. Store timestamps/attempt metadata if needed, use a bounded “processing timeout,” and reconcile with the authoritative side-effect system before retrying. For payment or order creation, the source of truth may prove that the operation already succeeded even though Redis still says PROCESSING.

8. Deliberately wrong: SETNX idempotency-key forever

A bare presence marker cannot distinguish processing from success, cannot validate that a retry body matches, cannot replay the original response, and can deadlock retries after a crashed worker. Repair with an explicit state machine, request fingerprint, bounded retention, result/reference storage, and reconciliation.

9. Idempotency is not duplicate suppression for every event forever

For Streams or message processing, idempotency may be based on event IDs and a business-effect ledger. At large cardinality, consider whether the durable database should enforce a unique constraint while Redis accelerates the hot retry path. Redis expiration is a bounded dedupe window, not an eternal historical ledger.

Check your understanding

  1. Why store a request fingerprint next to an idempotency key?
  2. What happens if the Redis idempotency record expires too early?
  3. Can Redis make an order-database commit and idempotency-record update one transaction?
  4. Why is a permanent SETNX marker insufficient?
Review the answers

To reject accidental or malicious reuse of the same key for different operation content.

A later duplicate may look new and repeat the business effect.

No. Reconciliation or a durable uniqueness/transaction mechanism is needed across systems.

It lacks state, request matching, replay data, crash recovery, and a bounded lifecycle.

10. Production judgment and bridge

Idempotency is appropriate for retryable APIs and message handlers where the same logical operation can arrive multiple times. Define principal/endpoint scope, canonical fingerprints, processing timeout, retention, authoritative reconciliation, response-size limits, privacy, and metrics for conflicts/duplicates/stuck processing. Lesson 5 then addresses a different concentration problem: many valid requests may all target the same Redis key.

Summary and next step

Idempotency Keys, Deduplication, Request State Machines, and Retry-Safe APIs is now connected to observable Redis behavior, bounded failure cases, and production tradeoffs. Keep the evidence and cleanup state from this lesson; next, continue with Hot Keys, Fan-Out, Replication Read Scaling, Sharding, Local Caches, and Workload Redesign.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.