Skip to content

Idempotency & De-dup: Designing Operations That Don't Double-Fire

Objective: Master idempotency and de-duplication patterns to prevent duplicate operations in distributed systems. When you need safe retries, when you want to prevent double-billing, when you're building reliable APIs and data pipelinesโ€”idempotency becomes your weapon of choice.

Retries happen (clients, proxies, workers). Without idempotency, you print money twiceโ€”or delete the same row twice. Build request identity, dedupe state, and committed outcomes into every layer.

0) Prerequisites (Read Once, Live by Them)

The Five Commandments

  1. Understand duplicate sources
  2. Client retries on timeout
  3. Load balancer resending
  4. At-least-once queues and job restarts
  5. Network splits and manual reruns
  6. System failures and recovery

  7. Master idempotency patterns

  8. HTTP APIs with Idempotency-Key headers
  9. Database UPSERT operations
  10. Background job deduplication
  11. Stream processing exactly-once semantics

  12. Know your deduplication strategies

  13. Request fingerprinting and caching
  14. Business key uniqueness constraints
  15. Content-addressed storage
  16. Event sourcing and replay

  17. Validate everything

  18. Test retry scenarios and edge cases
  19. Verify deduplication effectiveness
  20. Check audit trails and observability
  21. Monitor duplicate detection rates

  22. Plan for production

  23. Design for high-throughput scenarios
  24. Implement proper TTL policies
  25. Enable safe reprocessing and backfills
  26. Document rollback procedures

Why These Principles: Idempotency requires understanding both system reliability and data consistency patterns. Understanding these patterns prevents data corruption and enables reliable distributed systems.

1) Threat Model: Where Duplicates Come From

Duplicate Sources

flowchart LR
    C[Client] -- timeout+retry --> E[Edge / LB]
    E --> S[(Service)]
    subgraph Failure
        D[Duplicate delivery]
    end
    C -.replay after error.-> S

Why Threat Modeling Matters: Understanding duplicate sources enables systematic prevention strategies. Understanding these patterns prevents data corruption and enables reliable system design.

Common Duplicate Scenarios

# Common duplicate scenarios
duplicate_scenarios = {
    "client_retry": "Client retries on timeout without idempotency",
    "load_balancer": "LB resends requests to different instances",
    "queue_replay": "Message queues redeliver on failures",
    "job_restart": "Background jobs restart after crashes",
    "manual_rerun": "Operators manually rerun failed jobs",
    "network_split": "Network partitions cause duplicate operations"
}

Why Scenario Analysis Matters: Systematic analysis of duplicate sources enables comprehensive prevention strategies. Understanding these patterns prevents overlooked failure modes and enables robust system design.

2) HTTP APIs โ€” Idempotency Keys Done Right

FastAPI Implementation

# HTTP idempotency with Redis
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
from redis import Redis
import json, hashlib, time

app = FastAPI()
r = Redis.from_url("redis://localhost:6379/0")

class ChargeReq(BaseModel):
    user_id: str
    amount_cents: int

def cache_key(idem_key: str) -> str:
    return f"idem:{idem_key}"

@app.post("/charges")
def create_charge(payload: ChargeReq, idempotency_key: str = Header(..., alias="Idempotency-Key")):
    k = cache_key(idempotency_key)

    # Reserve key if new (SETNX)
    if r.set(k, json.dumps({"state":"pending"}), nx=True, ex=24*3600):
        # Do side effect once
        charge_id = f"ch_{hashlib.sha1((payload.user_id+str(payload.amount_cents)+str(time.time())).encode()).hexdigest()[:12]}"

        # Persist outcome
        outcome = {"status": 201, "charge_id": charge_id, "amount": payload.amount_cents}
        r.set(k, json.dumps({"state":"done","outcome": outcome}), ex=24*3600)
        return outcome

    # Key exists: return cached outcome or 409 if pending
    data = json.loads(r.get(k))
    if data["state"] == "pending":
        raise HTTPException(status_code=409, detail="Operation in progress")
    return data["outcome"]

Why Idempotency Keys Matter: SETNX reserves the key atomically. On retries you return cached results, you don't re-execute side effects.

Database Persistence

-- Idempotency audit table
CREATE TABLE idempotency_audit (
    idempotency_key TEXT PRIMARY KEY,
    request_fingerprint TEXT NOT NULL,
    outcome_hash TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at TIMESTAMPTZ NOT NULL
);

-- Index for cleanup
CREATE INDEX idx_idempotency_expires ON idempotency_audit(expires_at);

-- Cleanup expired keys
DELETE FROM idempotency_audit WHERE expires_at < now();

Why Database Persistence Matters: DB becomes the source of truth for idempotent state, not just the cache. Understanding these patterns prevents cache-only solutions and enables audit trails.

3) Database Writes โ€” UPSERT + Dedup Windows

Postgres UPSERT Pattern

-- Payouts table with idempotency
CREATE TABLE payouts (
    id BIGSERIAL PRIMARY KEY,
    idem_key TEXT UNIQUE NOT NULL,
    user_id TEXT NOT NULL,
    amount_cents INT NOT NULL,
    outcome_hash TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Safe insert: conflict on idem_key returns existing row
INSERT INTO payouts (idem_key, user_id, amount_cents, outcome_hash)
VALUES ($1, $2, $3, $4)
ON CONFLICT (idem_key) DO UPDATE
SET user_id = EXCLUDED.user_id
RETURNING *;

Why UPSERT Matters: DB becomes the source of truth for idempotent state, not just the cache. Understanding these patterns prevents race conditions and enables reliable data consistency.

Dedup Windows

-- Dedup window for sources without keys
CREATE TABLE external_transactions (
    id BIGSERIAL PRIMARY KEY,
    user_id TEXT NOT NULL,
    external_txn_id TEXT NOT NULL,
    amount_cents INT NOT NULL,
    processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Partial index for dedup window (last 24 hours)
CREATE UNIQUE INDEX idx_external_txn_dedup 
ON external_transactions (user_id, external_txn_id)
WHERE processed_at > now() - interval '24 hours';

Why Dedup Windows Matter: Time-bounded uniqueness prevents duplicates while allowing legitimate retries. Understanding these patterns prevents false positives and enables reliable deduplication.

4) Jobs & Schedulers โ€” Make Re-runs Harmless

Prefect Idempotent Tasks

# Prefect 2.x idempotent task
from prefect import flow, task
import psycopg
import hashlib, json

def fingerprint(row) -> str:
    return hashlib.sha1(json.dumps(row, sort_keys=True).encode()).hexdigest()

@task(retries=3)
def upsert_row(conn_str, row):
    fp = fingerprint(row)
    with psycopg.connect(conn_str) as con, con.cursor() as cur:
        cur.execute("""
          INSERT INTO facts (biz_key, payload_hash, payload)
          VALUES (%s, %s, %s)
          ON CONFLICT (biz_key) DO UPDATE
          SET payload_hash = EXCLUDED.payload_hash,
              payload = EXCLUDED.payload
          WHERE facts.payload_hash <> EXCLUDED.payload_hash
        """, (row["id"], fp, json.dumps(row)))

Why Job Idempotency Matters: Reruns won't duplicate facts; updates are conditional on changed hash. Understanding these patterns prevents data corruption and enables reliable job execution.

Run Ledger Pattern

-- Job run ledger
CREATE TABLE job_runs (
    run_id TEXT PRIMARY KEY,
    job_name TEXT NOT NULL,
    started_at TIMESTAMPTZ NOT NULL,
    completed_at TIMESTAMPTZ,
    status TEXT NOT NULL,
    business_key TEXT NOT NULL,
    outcome_hash TEXT
);

-- Idempotent job execution
INSERT INTO job_runs (run_id, job_name, started_at, status, business_key)
VALUES ($1, $2, now(), 'running', $3)
ON CONFLICT (run_id) DO NOTHING;

Why Run Ledger Matters: Job run tracking enables safe reruns and prevents duplicate execution. Understanding these patterns prevents job corruption and enables reliable automation.

5) Streams โ€” "Exactly-Once-ish" Outcomes

Kafka Producer Pattern

# Kafka producer with deterministic keys
from kafka import KafkaProducer
import json
import ulid

def produce_event(business_key, event_data):
    producer = KafkaProducer(
        bootstrap_servers=['localhost:9092'],
        value_serializer=lambda v: json.dumps(v).encode('utf-8')
    )

    event = {
        'event_id': str(ulid.ULID()),
        'business_key': business_key,
        'payload': event_data,
        'timestamp': time.time()
    }

    # Use business key for partitioning
    producer.send('events', value=event, key=business_key.encode())
    producer.flush()

Why Deterministic Keys Matter: Business key partitioning ensures all events for the same entity land in the same partition. Understanding these patterns prevents ordering issues and enables reliable stream processing.

Consumer Idempotent Sink

# Kafka consumer with idempotent sink
def handle_event(event):
    with db_connection() as con:
        # Dedupe by event_id
        con.execute("""
          INSERT INTO events (event_id, biz_key, payload)
          VALUES (%s, %s, %s)
          ON CONFLICT (event_id) DO NOTHING
        """, (event['event_id'], event['business_key'], event['payload']))

        # Update materialized view by business key
        con.execute("""
          INSERT INTO materialized_orders (biz_key, state)
          VALUES (%s, %s)
          ON CONFLICT (biz_key) DO UPDATE
          SET state = EXCLUDED.state
        """, (event['business_key'], project_state(event)))

Why Consumer Idempotency Matters: Dedupe by event_id and update materialized views by business key; safe to reprocess or rewind. Understanding these patterns prevents duplicate processing and enables reliable stream consumption.

Stream Processing Flow

flowchart LR
    K[(Kafka)] --> C[Consumer]
    C -->|event_id unique| E[(events table)]
    C -->|upsert by biz_key| M[(materialized view)]

Why Flow Diagrams Matter: Visual representation of stream processing enables understanding of data flow and consistency patterns. Understanding these patterns prevents processing errors and enables reliable stream architectures.

6) Object Stores โ€” Content Addressing FTW

S3 Content-Addressed Upload

#!/bin/bash
# S3 upload with content hash path
FILE="report.csv"
SHA=$(sha1sum "$FILE" | cut -d' ' -f1)
aws s3 cp "$FILE" "s3://bucket/reports/$SHA/$FILE"

Why Content Addressing Matters: Same bytes โ†’ same key. No duplicates, easy cache hits, trivial immutability. Understanding these patterns prevents storage waste and enables efficient content management.

Python Content-Addressed Storage

# Python content-addressed storage
import hashlib
import boto3

def upload_content_addressed(file_path, bucket, prefix=""):
    s3 = boto3.client('s3')

    # Calculate content hash
    with open(file_path, 'rb') as f:
        content = f.read()
        sha256_hash = hashlib.sha256(content).hexdigest()

    # Upload to content-addressed path
    key = f"{prefix}/{sha256_hash[:2]}/{sha256_hash[2:4]}/{sha256_hash}"
    s3.put_object(Bucket=bucket, Key=key, Body=content)

    return f"s3://{bucket}/{key}"

Why Python Implementation Matters: Programmatic content-addressed storage enables automated deduplication. Understanding these patterns prevents manual processes and enables scalable content management.

7) Reprocessing & Backfills โ€” The Hard Part

Watermark Table

-- Watermark tracking for reprocessing
CREATE TABLE watermarks (
    name TEXT PRIMARY KEY,
    position TEXT NOT NULL,     -- e.g., Kafka offset or timestamp
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Update watermark after successful processing
UPDATE watermarks 
SET position = $1, updated_at = now() 
WHERE name = $2;

Why Watermarks Matter: Watermark tracking enables safe reprocessing and prevents data loss. Understanding these patterns prevents reprocessing errors and enables reliable data recovery.

Versioned Projections

-- Versioned projections for safe backfills
CREATE TABLE orders_v2 (
    id BIGINT PRIMARY KEY,
    user_id TEXT NOT NULL,
    status TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Atomic swap after backfill
BEGIN;
ALTER TABLE orders RENAME TO orders_v1;
ALTER TABLE orders_v2 RENAME TO orders;
COMMIT;

Why Versioned Projections Matter: Atomic swaps enable safe backfills without downtime. Understanding these patterns prevents data corruption and enables reliable system updates.

8) Observability & Auditing

Idempotency Metrics

# Idempotency metrics collection
import time
from prometheus_client import Counter, Histogram, Gauge

# Metrics
idempotency_hits = Counter('idempotency_hits_total', 'Idempotency cache hits', ['operation'])
idempotency_misses = Counter('idempotency_misses_total', 'Idempotency cache misses', ['operation'])
operation_duration = Histogram('operation_duration_seconds', 'Operation duration', ['operation'])
dedupe_rate = Gauge('dedupe_rate', 'Deduplication success rate')

def track_idempotency(operation, hit):
    if hit:
        idempotency_hits.labels(operation=operation).inc()
    else:
        idempotency_misses.labels(operation=operation).inc()

Why Observability Matters: Metrics enable monitoring of idempotency effectiveness and system health. Understanding these patterns prevents silent failures and enables proactive system management.

Audit Logging

# Comprehensive audit logging
import logging
import json

def log_idempotency_operation(operation, idempotency_key, request_fingerprint, outcome_hash, duration):
    audit_log = {
        'timestamp': time.time(),
        'operation': operation,
        'idempotency_key': idempotency_key,
        'request_fingerprint': request_fingerprint,
        'outcome_hash': outcome_hash,
        'duration_ms': duration * 1000
    }

    logging.info(json.dumps(audit_log))

Why Audit Logging Matters: Comprehensive logging enables debugging and compliance. Understanding these patterns prevents investigation difficulties and enables regulatory compliance.

9) Security & TTLs

TTL Policy

# TTL configuration
IDEMPOTENCY_TTL = 24 * 3600  # 24 hours
AUDIT_TTL = 7 * 24 * 3600    # 7 days
CLEANUP_INTERVAL = 3600      # 1 hour

def cleanup_expired_keys():
    """Clean up expired idempotency keys"""
    expired_keys = r.keys(f"idem:*")
    for key in expired_keys:
        if r.ttl(key) == -1:  # No expiration set
            r.delete(key)

Why TTL Policies Matter: Proper TTL management prevents storage bloat and enables compliance. Understanding these patterns prevents resource exhaustion and enables efficient system operation.

Security Considerations

# Secure idempotency key validation
import re
import uuid

def validate_idempotency_key(key):
    """Validate idempotency key format"""
    # Must be UUID v4 or ULID
    uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
    ulid_pattern = r'^[0-9A-HJKMNP-TV-Z]{26}$'

    if re.match(uuid_pattern, key) or re.match(ulid_pattern, key):
        return True
    return False

Why Security Matters: Proper key validation prevents injection attacks and ensures system security. Understanding these patterns prevents security vulnerabilities and enables robust system design.

10) Anti-Patterns (Red Flags)

Common Anti-Patterns

# DON'T: Generate idempotency key on server
def bad_idempotency():
    idem_key = str(uuid.uuid4())  # Wrong! Must come from client
    # This defeats the purpose of idempotency

# DON'T: Use request body hash alone
def bad_fingerprint(request_body):
    return hashlib.sha256(request_body).hexdigest()  # Whitespace/order changes break it

# DON'T: Retry non-idempotent operations
def bad_retry():
    charge_card(amount)  # This will double-charge!
    # Always use idempotency keys for side effects

# DON'T: Delete and recreate
def bad_upsert():
    delete_row(id)
    insert_row(id, data)  # Breaks FKs and history
    # Use proper UPSERT instead

Why Anti-Patterns Matter: Common mistakes lead to data corruption and system failures. Understanding these patterns prevents implementation errors and enables reliable system design.

Correct Patterns

# DO: Use client-provided idempotency keys
def good_idempotency(idem_key_from_client):
    # Use the key provided by the client
    pass

# DO: Use stable fingerprints
def good_fingerprint(data):
    # Normalize data before hashing
    normalized = json.dumps(data, sort_keys=True)
    return hashlib.sha256(normalized.encode()).hexdigest()

# DO: Use proper UPSERTs
def good_upsert():
    # Use database UPSERT operations
    pass

Why Correct Patterns Matter: Proper implementation prevents data corruption and enables reliable systems. Understanding these patterns prevents common mistakes and enables robust system design.

11) TL;DR Runbook

Essential Commands

# HTTP API idempotency
curl -X POST /charges \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"user_id":"123","amount_cents":1000}'

# Database UPSERT
psql -c "INSERT INTO payouts (idem_key, user_id, amount_cents) VALUES ('key123', 'user456', 1000) ON CONFLICT (idem_key) DO UPDATE SET user_id = EXCLUDED.user_id;"

# Content-addressed storage
sha256=$(sha256sum file.txt | cut -d' ' -f1)
aws s3 cp file.txt "s3://bucket/$sha256/file.txt"

Essential Patterns

# Essential idempotency patterns
idempotency_patterns = {
    "http_apis": "Require Idempotency-Key for POST writes; cache outcome for 24h+",
    "database": "Use UPSERTs keyed by business identity; hash payloads to avoid rewrites",
    "streams": "Dedupe by event_id; materialize by biz_key",
    "objects": "Content-addressed paths (hash)",
    "reprocessing": "Versioned projections, watermarks, bounded windows",
    "observability": "Dedupe hit rate, conflicts, retries, replay progress"
}

Quick Reference

# Essential idempotency operations
# 1. HTTP API idempotency
@app.post("/charges")
def create_charge(payload: ChargeReq, idempotency_key: str = Header(..., alias="Idempotency-Key")):
    # Check cache, execute if new, return cached if exists
    pass

# 2. Database UPSERT
INSERT INTO table (idem_key, data) VALUES ($1, $2) ON CONFLICT (idem_key) DO UPDATE SET data = EXCLUDED.data;

# 3. Stream deduplication
INSERT INTO events (event_id, payload) VALUES ($1, $2) ON CONFLICT (event_id) DO NOTHING;

# 4. Content-addressed storage
sha256 = hashlib.sha256(content).hexdigest()
s3.put_object(Bucket=bucket, Key=sha256, Body=content)

# 5. Watermark tracking
UPDATE watermarks SET position = $1 WHERE name = $2;

Why This Runbook: These patterns cover 90% of idempotency needs. Master these before exploring advanced scenarios.

12) The Machine's Summary

Idempotency requires understanding both system reliability and data consistency patterns. When used correctly, idempotency prevents duplicate operations, enables safe retries, and maintains data integrity in distributed systems. The key is understanding request identity, mastering deduplication strategies, and following security best practices.

The Dark Truth: Without proper idempotency understanding, your system is fragile and unreliable. Idempotency is your weapon. Use it wisely.

The Machine's Mantra: "In idempotency we trust, in deduplication we prevent, and in the audit trail we find the path to system reliability."

Why This Matters: Idempotency enables efficient system operations that can handle complex retry scenarios, maintain data consistency, and provide reliable distributed systems while ensuring security and observability.


This guide provides the complete machinery for idempotency and de-duplication. The patterns scale from simple HTTP APIs to complex distributed systems, from basic retry handling to advanced stream processing.