Skip to content

Idempotency

Request deduplication for safe retries

Extension URN: urn:mesh:ext:idempotency


The idempotency extension ensures that retrying a request doesn’t cause duplicate side effects. Servers cache results keyed by the idempotency key and return cached results for duplicate requests.


Idempotency SHOULD be used for:

  • Payment processing
  • Order creation
  • Resource provisioning
  • Any operation with side effects
  • Network-unreliable environments

Idempotency is NOT needed for:

  • Read operations (naturally idempotent)
  • Operations without side effects

FieldTypeRequiredDescription
keystringYesUnique key for deduplication
ttlobjectNoRequested cache duration

Keys SHOULD include enough context to be unique per logical operation:

PatternExample
Entity + timestamporder_cust42_1702500000
Entity + operationcharge_order123_attempt1
Combined entitiesreserve_sku123_order456
UUID550e8400-e29b-41d4-a716-446655440000

FieldTypeDescription
keystringEchoed idempotency key
statusstringprocessed, cached, or processing
original_request_idstringRequest ID that first processed this key
cached_atstringWhen result was cached (if replayed)
expires_atstringWhen cached result expires

When a new idempotency key is received:

  1. Server MUST hash key with function + version
  2. Server MUST process the request normally
  3. Server MUST cache the result
  4. Server MUST return status: "processed"

When the same idempotency key is received again:

  1. Server MUST NOT reprocess the request
  2. Server MUST return the cached result
  3. Server MUST return status: "cached"
  4. Server MUST include original_request_id

When duplicate key received while original is processing:

  1. Server MUST NOT start new processing
  2. Server MUST return IDEMPOTENCY_PROCESSING error
  3. Client SHOULD retry after suggested delay

When same key is used with different arguments:

  1. Server MUST NOT process the request
  2. Server MUST return IDEMPOTENCY_CONFLICT error
  3. Server SHOULD include hash of original arguments

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_001",
"call": {
"function": "payments.charge",
"version": "1",
"arguments": {
"amount": 100,
"currency": "USD",
"customer_id": "cust_123"
}
},
"extensions": [
{
"urn": "urn:mesh:ext:idempotency",
"options": {
"key": "charge_order456_v1"
}
}
]
}
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_001",
"result": {
"charge_id": "ch_abc",
"status": "succeeded"
},
"extensions": [
{
"urn": "urn:mesh:ext:idempotency",
"data": {
"key": "charge_order456_v1",
"status": "processed",
"original_request_id": "req_001",
"expires_at": "2024-03-16T10:30:00Z"
}
}
]
}

Same idempotency key, different request ID:

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_002",
"result": {
"charge_id": "ch_abc",
"status": "succeeded"
},
"extensions": [
{
"urn": "urn:mesh:ext:idempotency",
"data": {
"key": "charge_order456_v1",
"status": "cached",
"original_request_id": "req_001",
"cached_at": "2024-03-15T10:30:00Z",
"expires_at": "2024-03-16T10:30:00Z"
}
}
]
}

Same key with different arguments:

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_003",
"result": null,
"errors": [{
"code": "IDEMPOTENCY_CONFLICT",
"message": "Idempotency key already used with different arguments",
"retryable": false,
"details": {
"key": "charge_order456_v1",
"original_arguments_hash": "sha256:abc123..."
}
}],
"extensions": [
{
"urn": "urn:mesh:ext:idempotency",
"data": {
"key": "charge_order456_v1",
"status": "conflict",
"original_request_id": "req_001"
}
}
]
}

Concurrent request while original is processing:

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_004",
"result": null,
"errors": [{
"code": "IDEMPOTENCY_PROCESSING",
"message": "Previous request with this key is still processing",
"retryable": true,
"details": {
"key": "charge_order456_v1",
"retry_after": { "value": 1, "unit": "second" }
}
}]
}

These serve different purposes:

FieldScopePurpose
idPer-attemptCorrelate request↔response
keyPer-operationDeduplicate retries

Three retries of the same operation:

Attempt 1: id="req_001", key="idem_abc" → Server processes
Attempt 2: id="req_002", key="idem_abc" → Server returns cached
Attempt 3: id="req_003", key="idem_abc" → Server returns cached

Different id values enable logging/tracing of each attempt. Same key prevents duplicate processing.


Servers MUST:

  • Store results keyed by hash(key + function + version)
  • Include request arguments hash for conflict detection
  • Set TTL on cached entries (recommended: 24 hours)
idempotency_cache {
key_hash: string (primary)
function: string
version: string
arguments_hash: string
result: json
status: enum(processing, completed, failed)
created_at: timestamp
expires_at: timestamp
}

CodeRetryableDescription
IDEMPOTENCY_CONFLICTNoKey reused with different arguments
IDEMPOTENCY_PROCESSINGYesPrevious request still processing