Skip to content
Ledgerline Help

Retries and backoff

Updated 6 September 20263 min read

On this page

Requests fail for reasons that have nothing to do with your code: a dropped connection, a busy upstream, a deploy in progress. A good client retries the right failures, at the right pace, safely. This page lists what to retry and how.

What to retry

Outcome Retry? Notes
Network error, connection reset, DNS failure Yes The request may or may not have reached us. Use an idempotency key.
Timeout on your side Yes The operation may have completed.
408 Request Timeout Yes
429 Too Many Requests Yes Wait for the Retry-After interval.
500 Internal Server Error Yes
502 Bad Gateway Yes
503 Service Unavailable Yes
504 Gateway Timeout Yes
Any other 4xx No The request is wrong. Retrying cannot fix it.

A 409 idempotency_conflict is the one 4xx worth retrying: it means the first attempt is still running. Retry it with the same key after a short wait.

Important

Only retry POST requests when you send an Idempotency-Key. Without one, a retry after a timeout can create a duplicate payout.

The backoff schedule

Use exponential backoff with full jitter. Wait a random time between zero and an exponentially growing ceiling.

Setting Recommended
Base delay 500 ms
Multiplier 2
Jitter Full: pick uniformly from 0 to the current ceiling
Maximum delay 30 seconds
Maximum attempts 5

That gives ceilings of 0.5, 1, 2, 4 and 8 seconds for the first five retries, never above 30 seconds. Jitter matters more than it looks: without it, every client that failed together retries together and hits the API as a synchronized wave.

async function withRetries<T>(fn: () => Promise<Response>, parse: (r: Response) => Promise<T>): Promise<T> {
  const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
  for (let attempt = 0; ; attempt++) {
    try {
      const res = await fn();
      if (res.ok) return parse(res);
      if (!RETRYABLE.has(res.status) || attempt >= 4) throw new Error(`HTTP ${res.status}`);
      const retryAfter = Number(res.headers.get("retry-after"));
      const ceiling = Math.min(30_000, 500 * 2 ** attempt);
      await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : Math.random() * ceiling);
    } catch (error) {
      if (attempt >= 4 || !isNetworkError(error)) throw error;
      await sleep(Math.random() * Math.min(30_000, 500 * 2 ** attempt));
    }
  }
}

Honor Retry-After

A 429 response includes Retry-After, the number of seconds until you should try again. Use it instead of your own schedule. The same header can appear on 503 during maintenance. See Rate limits.

Timeouts

The API server gives each request up to 60 seconds. Set your client timeout lower than that if you can tolerate it, and treat a timeout as an unknown outcome rather than a failure. The SDKs default to 30 seconds. If a POST times out, retry with the same idempotency key. The server returns the stored result if the first attempt finished.

SDK defaults

Both SDKs retry automatically with the policy above, using max_retries of 2 by default and a 30-second timeout. Raise or lower it when you create the client.

const ll = new Ledgerline(process.env.LEDGERLINE_API_KEY!, { maxRetries: 4, timeout: 20_000 });
client = ledgerline.Client(api_key=os.environ["LEDGERLINE_API_KEY"], max_retries=4, timeout=20)

If you wrap an SDK call in your own retry loop, keep the idempotency key constant across your attempts, and do not multiply retries so that one failure turns into dozens of requests.

Circuit breakers

When Ledgerline is unavailable, retries add load without helping. Add a circuit breaker that stops sending after consecutive failures, and check Status and incidents before you escalate. For webhook deliveries from Ledgerline to you, the retry policy is separate: see Webhook delivery and retries.

Checklist

  1. Send an Idempotency-Key on every POST.
  2. Retry only the status codes in the table.
  3. Back off with jitter, capped at 30 seconds, for at most 5 attempts.
  4. Honor Retry-After.
  5. Log the Ledgerline-Request-Id of every failed attempt.