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.
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.
Explain the mechanisms and terminology behind Idempotency Keys, Deduplication, Request State Machines, and Retry-Safe APIs.
Collect Redis, client, configuration, and workload evidence before drawing operational conclusions.
Reproduce the lesson's deliberately incorrect or failure-prone case, diagnose the mechanism, and verify the repair.
Relate the design to memory, persistence, replication/Sentinel/Cluster, security, latency, and client behavior where applicable.
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.
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
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.
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.
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.
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
- Why store a request fingerprint next to an idempotency key?
- What happens if the Redis idempotency record expires too early?
- Can Redis make an order-database commit and idempotency-record update one transaction?
- 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
- Redis cache-aside
- Redis cache-aside with redis-py
- Redis rate limiter
- Rate limiter with redis-py
- INCR — rate limiter pattern
- SET
- DELEX
- Distributed locks with Redis
- EVAL
- TIME
- EXPIRE
- PTTL
- ZADD
- ZREMRANGEBYSCORE
- HSET
- HGETALL
- OBJECT FREQ
- redis-cli
- Redis eviction reference
- Redis replication
- Redis Cluster specification
- Redis 8.10 release notes
- Redis licenses
- redis-py 8.1.0