A Traefik middleware plugin that implements RFC draft-ietf-httpapi-idempotency-key-header for idempotent HTTP requests using Redis for storage.
- RFC Compliant: Implements the IETF Idempotency-Key HTTP Header draft specification
- Redis Storage: Uses Redis for distributed storage of idempotency records
- Concurrent Request Handling: Properly handles concurrent duplicate requests with 409 Conflict
- Request Fingerprinting: Detects when the same key is reused with different payloads (422 Unprocessable Content)
- Response Caching: Caches and replays responses for duplicate requests
- RFC 7807 Errors: Returns problem details format for error responses
- Binary-Safe: Properly handles binary response bodies using base64 encoding
- Multi-Value Headers: Preserves all header values including Set-Cookie
- Stale Lock Recovery: Automatically recovers from crashed requests via configurable lock timeout
┌─────────────┐ ┌──────────────────────┐ ┌─────────┐
│ Client │────▶│ Idempotency Plugin │────▶│ Backend │
└─────────────┘ └──────────────────────┘ └─────────┘
│
▼
┌─────────┐
│ Redis │
└─────────┘
- First request: Acquires lock, processes request, caches response
- Duplicate (completed): Returns cached response immediately
- Concurrent retry: Returns 409 Conflict if original still processing
- Stale lock: If processing lock exceeds
lockTimeout, overwrites and reprocesses
Add to your Traefik static configuration:
experimental:
plugins:
idempotency:
moduleName: github.com/useswype/traefik-idempotency-plugin
version: v1.0.0Place the plugin in your Traefik plugins directory:
./plugins-local/
└── src
└── github.com
└── useswype
└── traefik-idempotency-plugin
├── *.go
├── go.mod
└── .traefik.yml
experimental:
localPlugins:
idempotency:
moduleName: github.com/useswype/traefik-idempotency-plugin| Option | Type | Default | Description |
|---|---|---|---|
redisAddress |
string | localhost:6379 |
Redis server address (host:port) |
redisPassword |
string | "" |
Redis authentication password |
redisDB |
int | 0 |
Redis database number (0-15) |
keyPrefix |
string | idempotency: |
Redis key prefix |
keyHeader |
string | Idempotency-Key |
Header name for the idempotency key |
ttlSeconds |
int | 86400 |
TTL for stored records (24 hours) |
requireKey |
bool | false |
Return 400 if key is missing |
methods |
[]string | ["POST", "PATCH"] |
HTTP methods to enforce idempotency |
connectionTimeout |
int | 5000 |
Redis connection timeout (ms) |
poolSize |
int | 10 |
Max Redis connections to pool |
maxBodySize |
int | 1048576 |
Max body size to buffer (1MB) |
lockTimeout |
int | 60 |
Seconds before processing lock is stale |
failOpen |
bool | true |
Allow requests through if Redis is unavailable |
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: idempotency
namespace: default
spec:
plugin:
idempotency:
redisAddress: "redis-master.redis.svc.cluster.local:6379"
redisPassword: ""
redisDB: 0
keyHeader: "Idempotency-Key"
ttlSeconds: 86400
requireKey: false
methods:
- POST
- PATCH
- PUT
keyPrefix: "idempotency:"
connectionTimeout: 5000
poolSize: 10
maxBodySize: 1048576
lockTimeout: 60
failOpen: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: api
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`api.example.com`)
kind: Rule
middlewares:
- name: idempotency
services:
- name: api-service
port: 80# First request - creates the resource
curl -X POST https://api.example.com/orders \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: "8e03978e-40d5-43e8-bc93-6894a57f9324"' \
-d '{"amount": 100, "currency": "USD"}'
# Response: 201 Created
# {"id": "order-123", "amount": 100, "currency": "USD"}
# Retry with same key - returns cached response instantly
curl -X POST https://api.example.com/orders \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: "8e03978e-40d5-43e8-bc93-6894a57f9324"' \
-d '{"amount": 100, "currency": "USD"}'
# Response: 201 Created (from cache)
# {"id": "order-123", "amount": 100, "currency": "USD"}Per RFC 8941, the idempotency key should be a quoted string:
Idempotency-Key: "8e03978e-40d5-43e8-bc93-6894a57f9324"
The plugin also accepts unquoted values for convenience:
Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324
Valid key characters: a-z, A-Z, 0-9, -, _
All errors follow RFC 7807 Problem Details format:
400 Bad Request - Missing Key (when required)
{
"type": "about:blank#missing-idempotency-key",
"title": "Bad Request",
"status": 400,
"detail": "The Idempotency-Key header is required for this request"
}409 Conflict - Concurrent Request
{
"type": "about:blank#request-in-progress",
"title": "Conflict",
"status": 409,
"detail": "A request with this Idempotency-Key is currently being processed"
}422 Unprocessable Content - Key Reused
{
"type": "about:blank#idempotency-key-reused",
"title": "Unprocessable Content",
"status": 422,
"detail": "The Idempotency-Key has already been used with a different request payload"
}503 Service Unavailable - Storage Unavailable (when failOpen=false)
{
"type": "about:blank#storage-unavailable",
"title": "Service Unavailable",
"status": 503,
"detail": "The idempotency storage is temporarily unavailable"
}This plugin uses a custom minimal Redis client built on Go's standard library. This design choice was made because:
- Traefik plugins run via Yaegi (Go interpreter), which has compatibility issues with popular Redis client libraries
- The custom client implements only the necessary RESP protocol commands
- Connection pooling is included to minimize connection overhead