Error Handling Patterns

Idempotency Keys for Safe API Retries

How idempotency keys let clients safely retry mutating API requests without creating duplicate resources or double-charging users.

The Double-Submit Problem

Consider a payment API. The client sends a charge request, but the network drops before the response arrives. The payment processed — the client just never got the confirmation. What does the client do?

  • Don't retry: The user thinks the payment failed. They try again manually. Now they're charged twice.
  • Retry blindly: The server processes two payments. The user is charged twice.

Idempotency keys solve this by allowing safe retries of mutating requests.

What Are Idempotency Keys?

An idempotency key is a unique client-generated identifier sent with every mutating request. The server uses this key to detect duplicate requests and return the same response as the first successful attempt, without re-executing the operation.

POST /charges HTTP/1.1
Idempotency-Key: a4e1b2c3-d4e5-6789-abcd-ef0123456789
Content-Type: application/json

{"amount": 5000, "currency": "usd", "customer": "cus_abc123"}

If the client retries with the same key, the server returns the original response — no duplicate charge.

Server-Side Implementation

Step 1: Storage

Store idempotency records with: key, request fingerprint, response status, response body, and creation timestamp.

from django.db import models

class IdempotencyRecord(models.Model):
    key = models.CharField(max_length=255, unique=True)
    request_fingerprint = models.CharField(max_length=64)
    response_status = models.IntegerField()
    response_body = models.JSONField()
    created_at = models.DateTimeField(auto_now_add=True)

Step 2: Request Handling

def charge_view(request):
    idempotency_key = request.headers.get('Idempotency-Key')
    if not idempotency_key:
        return HttpResponse(status=400, content='Idempotency-Key required')
    # Check for existing record
    existing = IdempotencyRecord.objects.filter(
        key=idempotency_key
    ).first()
    if existing:
        return JsonResponse(existing.response_body,
                           status=existing.response_status)
    # Process the charge
    result = process_charge(request.data)
    # Store the result
    IdempotencyRecord.objects.create(
        key=idempotency_key,
        response_status=200,
        response_body=result,
    )
    return JsonResponse(result)

Step 3: TTL

Idempotency records do not need to live forever. Stripe keeps them for 24 hours. Choose a TTL that covers your retry window:

# Django management command to clean up expired records
from django.utils import timezone
from datetime import timedelta

IdempotencyRecord.objects.filter(
    created_at__lt=timezone.now() - timedelta(hours=24)
).delete()

Step 4: Concurrency

Two concurrent requests with the same key (a network retry arriving while the original is still processing) must be handled carefully:

  • Use a database unique constraint on the key
  • Use SELECT FOR UPDATE or Redis lock during processing
  • Return 409 Conflict if a request with the same key is in-flight

Client-Side Best Practices

  • Generate keys client-side: Use UUID v4 — never rely on the server
  • Use one key per logical operation: Don't reuse keys across different operations
  • Retry on network errors and 5xx: Do NOT retry on 4xx (the request itself is wrong)
  • Include the key in logs: Essential for debugging 'did it process?' questions

Stripe Case Study

Stripe popularized the pattern. Their implementation:

  • Header: Idempotency-Key: <your-key> (required for POST requests)
  • TTL: 24 hours
  • Key mismatch: If the same key is sent with different request bodies, Stripe returns 422 Unprocessable Entity
  • Response: The original response is replayed exactly, including headers

Status Codes

StatusMeaning
**200**Replayed response from original successful request
**409 Conflict**Same key currently being processed (in-flight)
**422 Unprocessable Entity**Key reused with different request body

Summary

Idempotency keys are essential for any API that accepts payments, sends emails, or creates resources. Require the Idempotency-Key header on all POST endpoints that have side effects, store results with a TTL, handle concurrent requests with database locks, and validate that the key is not reused with different request bodies.

Giao thức liên quan

Thuật ngữ liên quan

Thêm trong Error Handling Patterns