Concepts
Retries and backoff
What the SDKs retry on their own (429, 502, 503, 504 with exponential backoff, jitter and Retry-After), why writes aren’t retried by default, and how to enable it safely.
On this page
All five SDKs ship with the same retry policy, designed so that retrying never duplicates an operation.
The default policy #
| Parameter | Default value | What it does |
|---|---|---|
| Maximum attempts | 3 (the first + 2 retries) | 1 disables retries. |
| Backoff base | 0.5 s | Wait before the first retry, doubling on each one. |
| Backoff cap | 30 s | No wait exceeds this value. |
| Jitter | full | Each wait is random between 0 and the calculated value, so clients don’t all synchronize. |
Retry-After | honored | If the gateway sends it, the SDK waits exactly that long. |
| Statuses that are retried | 429, 502, 503, 504 | Transient failures. Never business 4xx or 500. |
| Methods | GET only | Writes are retried only with retry_writes enabled. |
| Network errors with no response | not retried | You 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] --> AWhy 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),
)const client = new CompenseClient({
apiKey: process.env.COMPENSE_API_KEY!,
environment: "sandbox",
retry: { maxAttempts: 5, backoffBaseMs: 500, backoffMaxMs: 30_000, jitter: true, retryWrites: true },
});use Compense\Sdk\CompenseClient;
use Compense\Sdk\RetryConfig;
$client = new CompenseClient(
apiKey: getenv('COMPENSE_API_KEY'),
environment: 'sandbox',
retry: new RetryConfig(maxAttempts: 5, backoffBase: 0.5, backoffMax: 30.0, jitter: true, retryWrites: true),
);var client = new CompenseClient(new CompenseClientOptions
{
ApiKey = Environment.GetEnvironmentVariable("COMPENSE_API_KEY")!,
Environment = CompenseEnvironment.Sandbox,
Retry = new RetryOptions
{
MaxAttempts = 5,
BackoffBase = TimeSpan.FromSeconds(0.5),
BackoffMax = TimeSpan.FromSeconds(30),
Jitter = true,
RetryWrites = true,
},
});client, err := compense.NewClient(os.Getenv("COMPENSE_API_KEY"),
compense.WithEnvironment(compense.Sandbox),
compense.WithRetry(compense.RetryConfig{MaxAttempts: 5, RetryWrites: true}),
// equivalent that only turns on writes: compense.WithRetryWrites(true)
)# With plain HTTP, retrying is up to you. Retry only 429/502/503/504 and network errors,
# honoring Retry-After, and ALWAYS with the same Idempotency-Key.
curl --retry 4 --retry-max-time 60 -X POST https://sandbox.dev.compense.co/gw/v1/gateway/loans/CRD-00123/payments/ \
-H "X-API-Key: $COMPENSE_API_KEY" -H "Idempotency-Key: horizonte-rec-2026-001" \
-H "Content-Type: application/json" \
-d '{"amount":"467009.88","reference":"REC-2026-001","channel":"CASH"}'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 #
| Failure | The SDK | You |
|---|---|---|
429 | Retries (GET, or writes with retry_writes) honoring Retry-After. | If attempts run out, queue the work and slow down (Limits). |
502/504 | Retries the same way as 429. | If it persists, retry later with the same key and give the trace_id. |
503 | Retries. | Same as 502. |
500 | Doesn’t retry. | Report the trace_id to support. |
In-flight 409 | Doesn’t retry. | Wait and repeat with the same key. |
Business 4xx | Doesn’t retry. | Fix the request or the case: retrying won’t fix it. |
| Network / timeout | Doesn’t retry. | Resend with the same Idempotency-Key. |