All five SDKs ship with the same retry policy, designed so that retrying never duplicates an operation.

The default policy #

ParameterDefault valueWhat it does
Maximum attempts3 (the first + 2 retries)1 disables retries.
Backoff base0.5 sWait before the first retry, doubling on each one.
Backoff cap30 sNo wait exceeds this value.
JitterfullEach wait is random between 0 and the calculated value, so clients don’t all synchronize.
Retry-AfterhonoredIf the gateway sends it, the SDK waits exactly that long.
Statuses that are retried429, 502, 503, 504Transient failures. Never business 4xx or 500.
MethodsGET onlyWrites are retried only with retry_writes enabled.
Network errors with no responsenot retriedYou resend them with the same Idempotency-Key.
flowchart TD
  A[Request] --> B{Response?}
  B -- no: timeout / network --> X[Transport exception<br/>resend it yourself with the same key]
  B -- yes --> C{429/502/503/504?}
  C -- no --> D[Success or typed error]
  C -- yes --> E{GET or retry_writes?}
  E -- no --> D
  E -- yes --> F{Attempts left?}
  F -- no --> D
  F -- yes --> G[Wait Retry-After<br/>or backoff with jitter] --> A

Why aren’t writes retried automatically? #

The gateway does deduplicate: every write with a repeated Idempotency-Key returns the stored response (idempotent_replay: true) instead of executing again, on every route that takes a key. The reason is different: the SDK can’t know which version of the gateway it’s talking to, and retrying a write against an older deployment without that guarantee would execute it twice. That’s why the safe default is to retry only GET.

Once you know your gateway deduplicates (every Compense environment does as of the current contract version), enable write retries. The SDK reuses the same Idempotency-Key across every attempt of a single call, so the server turns the retry into a replay:

from compense_gateway import CompenseClient, RetryConfig

client = CompenseClient(
    api_key=os.environ["COMPENSE_API_KEY"],
    environment="sandbox",
    retry=RetryConfig(max_attempts=5, backoff_base=0.5, backoff_max=30.0, jitter=True,
                      retry_writes=True),
)

In .NET with AddCompense, the same policy is configured in appsettings.json ("Compense": { "Retry": { "MaxAttempts": 5, "RetryWrites": true } }); in Go, a zero-valued field in RetryConfig takes the default value.

Timeouts #

The timeout is per attempt: Python timeout= (seconds), JS timeout (milliseconds), PHP timeout: (seconds, when the SDK builds Guzzle), .NET Timeout (TimeSpan), Go WithTimeout (or each call’s context.Context, which also cuts short the waits between retries). A timeout is an error with no response: you don’t know whether the write was applied, so resend it with the same key (Idempotency).

What to do with each failure #

FailureThe SDKYou
429Retries (GET, or writes with retry_writes) honoring Retry-After.If attempts run out, queue the work and slow down (Limits).
502/504Retries the same way as 429.If it persists, retry later with the same key and give the trace_id.
503Retries.Same as 502.
500Doesn’t retry.Report the trace_id to support.
In-flight 409Doesn’t retry.Wait and repeat with the same key.
Business 4xxDoesn’t retry.Fix the request or the case: retrying won’t fix it.
Network / timeoutDoesn’t retry.Resend with the same Idempotency-Key.