DEV Community

Cover image for Idempotency in APIs - Why Your Retry Logic Can Break Everything (And How to Fix It)
Fazal Mansuri
Fazal Mansuri

Posted on

Idempotency in APIs - Why Your Retry Logic Can Break Everything (And How to Fix It)

Modern applications rely heavily on APIs. But networks are unreliable - requests fail, time out, or get retried.

Now imagine this scenario:

  • User clicks β€œPay Now”
  • Request sent to server
  • Payment processed successfully
  • But response is lost due to network issue
  • Client retries the request

πŸ’₯ Payment gets processed again. User is charged twice.

This is one of the most dangerous and common problems in distributed systems.

The solution? Idempotency.


What is Idempotency?

An operation is idempotent if performing it multiple times produces the same result as performing it once.

In simple terms:

Same request β†’ same effect β†’ no duplicate side effects

Example:

Create Order (without idempotency):
Request sent twice β†’ 2 orders created ❌

Create Order (with idempotency):
Request sent twice β†’ only 1 order created βœ…
Enter fullscreen mode Exit fullscreen mode

Idempotency ensures system correctness even when retries happen.


Why Do Retries Happen?

Retries are extremely common due to:

  • Network timeouts
  • Server crashes
  • Client retries
  • Load balancers retrying requests
  • Mobile networks instability
  • API gateway retries
  • Reverse proxy retries

Retries are normal. Duplicate side effects are not.


Which HTTP Methods Are Idempotent?

According to HTTP specification:

Method Idempotent Explanation
GET βœ… Yes Fetch data
PUT βœ… Yes Replace resource
DELETE βœ… Yes Delete resource
POST ❌ No Create resource
PATCH ❌ No Partial update

POST is NOT idempotent by default.

Example:

POST /orders
Enter fullscreen mode Exit fullscreen mode

Calling twice β†’ creates 2 orders.

This must be handled explicitly.


Real-World Example: Payment API Problem

Client sends:

POST /payments
{
  "user_id": "123",
  "amount": 100
}
Enter fullscreen mode Exit fullscreen mode

If client retries due to timeout:

POST /payments
Enter fullscreen mode Exit fullscreen mode

Without idempotency β†’ duplicate payment ❌

With idempotency β†’ single payment βœ…


Solution: Idempotency Key

Client sends a unique key with request:

POST /payments
Idempotency-Key: abc123
Enter fullscreen mode Exit fullscreen mode

Server behavior:

  • If key is new β†’ process request
  • If key already exists β†’ return previous result

This guarantees single execution.


How Idempotency Works Internally

Flow:

  1. Client generates unique key
  2. Sends request with key
  3. Server checks database/cache for key
  4. If key exists β†’ return stored response
  5. If not exists β†’ process request
  6. Store key and response
  7. Return response

Go Implementation Example

Let’s implement idempotent payment API using Go.

Database Table

idempotency_keys table:

key (primary key)
response
created_at
Enter fullscreen mode Exit fullscreen mode

Go Example (Basic Version)

type IdempotencyRecord struct {
    Key      string
    Response string
}
Enter fullscreen mode Exit fullscreen mode

Handler Implementation

func PaymentHandler(w http.ResponseWriter, r *http.Request) {

    key := r.Header.Get("Idempotency-Key")

    if key == "" {
        http.Error(w, "Idempotency-Key required", http.StatusBadRequest)
        return
    }

    // Check if key exists
    record, exists := getIdempotencyRecord(key)

    if exists {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(record.Response))
        return
    }

    // Process payment
    paymentID := processPayment()

    response := fmt.Sprintf(`{"payment_id": "%s"}`, paymentID)

    // Store key and response
    saveIdempotencyRecord(key, response)

    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(response))
}
Enter fullscreen mode Exit fullscreen mode

Example Flow

First request:

POST /payments
Idempotency-Key: abc123

Response:
{
  "payment_id": "pay_001"
}
Enter fullscreen mode Exit fullscreen mode

Retry request:

POST /payments
Idempotency-Key: abc123
Enter fullscreen mode Exit fullscreen mode

Response returned from storage:

{
  "payment_id": "pay_001"
}
Enter fullscreen mode Exit fullscreen mode

No duplicate payment created.


Production Implementation Using Redis (Recommended)

Redis is ideal because:

  • Extremely fast
  • Supports expiration (TTL)
  • Perfect for temporary idempotency storage

Redis Example in Go

func PaymentHandler(w http.ResponseWriter, r *http.Request) {

    key := r.Header.Get("Idempotency-Key")

    val, err := redisClient.Get(ctx, key).Result()

    if err == nil {
        w.Write([]byte(val))
        return
    }

    paymentID := processPayment()

    response := fmt.Sprintf(`{"payment_id": "%s"}`, paymentID)

    redisClient.Set(ctx, key, response, time.Hour*24)

    w.Write([]byte(response))
}
Enter fullscreen mode Exit fullscreen mode

Important: Handle Race Conditions

Two requests may arrive simultaneously with same key.

Solution: Use atomic operations.

Redis example:

SET key value NX
Enter fullscreen mode Exit fullscreen mode

NX = set only if not exists


Important Best Practice: Store Full Response

Do not just store key.

Store:

  • status code
  • response body
  • timestamp

Because retry must return exact same response.


Example Database Schema (PostgreSQL)

CREATE TABLE idempotency_keys (
    key TEXT PRIMARY KEY,
    response JSONB,
    status_code INT,
    created_at TIMESTAMP DEFAULT NOW()
);
Enter fullscreen mode Exit fullscreen mode

Primary key ensures uniqueness.


Real-World Systems That Use Idempotency

Critical systems use idempotency extensively:

  • Payment APIs
  • Order creation APIs
  • Financial transactions
  • Booking systems
  • Wallet transfers
  • Stripe payments
  • Banking APIs

Without idempotency, financial systems would break.


Common Mistake Developers Make

❌ Using retry without idempotency

retry 3 times on failure
Enter fullscreen mode Exit fullscreen mode

This multiplies side effects.

Example:

Retry logic: 3 times
Result: 3 payments
Enter fullscreen mode Exit fullscreen mode

Dangerous bug.


Idempotency vs Duplicate Prevention Using Unique Constraints

Example:

UNIQUE(user_id, order_id)
Enter fullscreen mode Exit fullscreen mode

This prevents duplicates at DB level.

But idempotency is better because:

  • Works at API level
  • Handles retries cleanly
  • Returns same response

Best approach: use BOTH.


When Should You Use Idempotency?

Use idempotency for operations that create side effects:

  • Payment creation
  • Order creation
  • Wallet transfer
  • Booking
  • Resource creation

Not required for:

  • GET requests
  • Read-only operations

How Client Should Generate Idempotency Key

Use UUID:

Example:

550e8400-e29b-41d4-a716-446655440000
Enter fullscreen mode Exit fullscreen mode

Go example:

uuid.New().String()
Enter fullscreen mode Exit fullscreen mode

Unique per operation.


Idempotency Key Expiration Strategy

Keys should expire after some time.

Common TTL:

  • 24 hours
  • 48 hours
  • 7 days (depending on business need)

Redis makes expiration easy.


Example Architecture

Client β†’ API Gateway β†’ Service β†’ Redis β†’ Database

Redis handles idempotency keys
Database handles actual data

Fast and reliable.


Real Production Scenario: Mobile Network Retry

Mobile user taps "Place Order"

Network slow β†’ timeout
Client retries automatically

Without idempotency β†’ duplicate orders

With idempotency β†’ safe retry


Important Difference: Safe Retry vs Unsafe Retry

Unsafe retry:

POST /orders
Enter fullscreen mode Exit fullscreen mode

Safe retry:

POST /orders
Idempotency-Key: unique-key
Enter fullscreen mode Exit fullscreen mode

Idempotency vs Exactly-Once Execution

Exactly-once execution is impossible in distributed systems.

Idempotency provides practical solution.

It ensures same final state.


Best Practices Checklist

Always:

  • Use idempotency keys for POST APIs
  • Store keys in Redis or database
  • Store full response
  • Use expiration
  • Handle race conditions
  • Use UUID keys
  • Combine with database constraints

Key Takeaway

Retries are normal.

Duplicate side effects are dangerous.

Idempotency ensures your APIs remain safe, reliable, and production-ready.

It is not optional for critical APIs - it is essential.


Final Thought

If your API handles payments, orders, bookings, or financial transactions - idempotency is mandatory.

Without it, retries can silently corrupt your system.

With it, your APIs become reliable, predictable, and safe.

Top comments (0)