Skip to main content

Retry

retryInterceptor retries transient failures with exponential backoff and jitter. It is opt-in at registration time: include it in withInterceptors() and it applies to every request that qualifies.

provideHttpClient(withInterceptors([apiErrorInterceptor, retryInterceptor]));

What gets retried​

A request is retried only when all of these hold:

  1. Retry is not disabled globally (retry: false in provideApi()).
  2. The request did not opt out with retry: false.
  3. The method is idempotent — POST is skipped unless the request opts in.
  4. The response status is in retryableStatuses.
  5. The failure still is an HttpErrorResponse when it reaches the interceptor — see Ordering.

The default statuses are 408, 429, 500, 502, 503, 504. A 4xx outside that list is a client mistake: retrying it just repeats the mistake.

Network failures are not retried by default

A status 0 failure does not appear in retryableStatuses. Add it explicitly if your app should retry dropped connections: retryableStatuses: [0, 408, 429, 500, 502, 503, 504].

Backoff​

BehaviourDetail
DelayinitialDelay * multiplier^(attempt-1)
JitterPlus a random 0–30% of that delay, to avoid a thundering herd
Retry-AfterHonoured when present — in seconds or as an HTTP date — and it wins
AttemptsmaxRetries retries after the initial attempt

With the defaults (initialDelay: 1000, multiplier: 2), the delays before attempts 2, 3 and 4 are roughly 1s, 2s and 4s, each nudged upward by jitter.

Configuration​

provideApi({
baseUrl: 'https://api.example.com',
retry: {
maxRetries: 3,
initialDelay: 1000,
multiplier: 2,
retryableStatuses: [408, 429, 500, 502, 503, 504],
},
});
OptionDefaultDescription
maxRetries3Attempts after the first one
initialDelay1000Milliseconds before the first retry
multiplier2Applied to the delay after each attempt
retryableStatuses[408, 429, 500, 502, 503, 504]Statuses that trigger a retry

Disable it everywhere with retry: false.

Per-request control​

// Never retry this one
this.api.get<Report>('/reports/heavy', { retry: false });

// Opt a POST in — safe because the endpoint is idempotent by design
this.api.post<Order>('/orders', draft, { retry: true });

// Be more patient for one slow endpoint
this.api.get<Export>('/exports/full', {
retry: { maxRetries: 5, initialDelay: 2000 },
});

A per-request RetryConfig object is merged over the built-in defaults, not over your global configuration — so spell out every field you care about.

Why POST is excluded​

POST is not idempotent: a request that timed out may well have been processed, and retrying it can create a second record. The interceptor skips it unless the call opts in with retry: true or a RetryConfig. Use that opt-in when the endpoint is genuinely safe to repeat — for example when it is guarded by an idempotency key:

this.api.post<Payment>('/payments', body, {
retry: true,
headers: { 'Idempotency-Key': idempotencyKey },
});

PUT, PATCH and DELETE are treated as idempotent and retried normally.

Ordering​

The first interceptor in the array is the outermost one. retryInterceptor therefore belongs after apiErrorInterceptor and before any auth interceptor:

withInterceptors([
apiErrorInterceptor, // outermost — sees the failure that survived every retry
apiSuccessInterceptor,
retryInterceptor,
bearerTokenInterceptor, // innermost — re-signs each attempt
]);
Getting this backwards disables retry

retryInterceptor only retries an HttpErrorResponse. apiErrorInterceptor replaces that with a plain ApiError object, so if it runs inside the retry interceptor there is nothing left to match — every request gets exactly one attempt and no retry ever fires. The failure is silent: requests still work, they just stop being resilient.

// ❌ retry never happens — apiErrorInterceptor is nested inside it
withInterceptors([retryInterceptor, apiErrorInterceptor]);

// ✅ retry works, and the handler fires once per failed operation
withInterceptors([apiErrorInterceptor, retryInterceptor]);

Putting the auth interceptor innermost means the retried attempts re-run it, so each one is signed with a fresh token rather than replaying an expired one.

Interaction with the loading indicator​

The loading counter is incremented once per ApiService call, not once per HTTP attempt, so a request that retries three times keeps the indicator up for the whole operation instead of flickering between attempts.