← Retour au blog
AngularHTTPInterceptorsRetryBackoffResilienceRxJS

Advanced REST: HTTP Interceptors and Exponential Retry Backoff

Modern applications no longer speak to the server naively. An HTTP request can fail for a thousand reasons: network timeout, momentary server overload, connectivity loss, rate limiting. Waiting for the user to refresh the page or manually retry is unacceptable. Angular's HTTP interceptors, combined with an intelligent retry strategy, transform a fragile application into a resilient system that absorbs transient failures without impacting UX. This is the difference between an app that frustrates and one that inspires confidence.

Anatomy of HTTP Interceptors

HTTP interceptors in Angular sit in the pipeline and intercept every request and response. Unlike backend middleware, they live on the client and control inbound and outbound flow. An interceptor receives an HttpRequest , can modify it, block it, or pass it forward via next.handle() . On the response side, it can inspect status, capture errors, or transform data. Creating an interceptor requires a class implementing HttpInterceptor and injection via HTTP_INTERCEPTORS . The beauty lies in execution order: you can stack multiple interceptors and chain them logically. One for logging, another for retry, a third for auth—each plays its role without knowing the others. It's a micro-middleware architecture on the client side.

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log(`[HTTP] ${req.method} ${req.url}`);
    return next.handle(req);
  }
}

Retry with Exponential Backoff: Real Resilience

A simple linear retry (wait 1s, 2s, 3s) is not enough in production. If the server is overloaded and a hundred clients retry simultaneously with the same delay, you create a "thundering herd" that kills the server. Exponential backoff solves this: wait 100ms, then 200ms, then 400ms, then 800ms—delays double with each attempt. Adding jitter (randomization) prevents accidental synchronization between clients. The RxJS operator retryWhen lets you capture errors and decide whether to retry. Combining delay() with a counter and a cap creates a robust, predictable system. In practice, retry on 5xx errors and timeouts, but not 4xx (except 429 for rate limiting).

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { retryWhen, mergeMap } from 'rxjs/operators';

@Injectable()
export class RetryBackoffInterceptor implements HttpInterceptor {
  private maxRetries = 3;
  private baseDelay = 100; // ms

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      retryWhen(errors =>
        errors.pipe(
          mergeMap((error, attempt) => {
            // Only retry 5xx and timeouts
            if (!this.isRetryable(error) || attempt >= this.maxRetries) {
              return throwError(() => error);
            }
            const delayMs = this.baseDelay * Math.pow(2, attempt);
            const jitter = Math.random() * delayMs * 0.1; // ±10%
            return new Promise(resolve => setTimeout(resolve, delayMs + jitter));
          })
        )
      )
    );
  }

  private isRetryable(error: any): boolean {
    return error.status >= 500 || error.status === 0; // 0 = timeout or offline
  }
}

Circuit Breaker: Preventing Cascading Failures

Retry backoff is powerful but insufficient during prolonged outages. If the server is truly down, retrying for 10 seconds only delays the inevitable and wastes resources. A circuit breaker works like an electrical breaker: after N successive errors, it "breaks the circuit" and immediately rejects requests for a delay (half-open state). If the server recovers, you gradually shift back to normal mode. Implementing a circuit breaker requires more logic: track errors, remember circuit state, and add a timeout before attempting reconnection. The ngx-circuit-breaker library exists, but writing your own is instructive and customizable. Rate-limiting requests when the server is degraded also protects the backend and improves overall user experience.

Common Pitfalls and Anti-Patterns

The classic pitfall: retry *all* requests indiscriminately. A POST without idempotence can create duplicates. A 400 (bad request) doesn't improve with retries. An overly aggressive retry interceptor delays showing real errors to the user. Another pitfall: forgetting jitter. If 1,000 clients retry at the same moment with the same exponential delay, you synchronize requests instead of spreading them out. Jitter (random ±10%) eliminates this. Finally, failing to monitor interceptors in production. If an interceptor silently bugs or logs badly, you have no visibility into what's actually happening. Adding structured logs (timestamp, endpoint, retry count) to interceptors is crucial. Using an observability service (Datadog, New Relic) to track failure patterns at scale lets you detect systemic issues before they become critical.

Integration with Authentication

Interceptors also play a key role in authentication. An auth interceptor adds the JWT token to every request. If the token expires and the server returns 401, should you retry? Yes, but only after refreshing the token. This requires coordination between the auth and retry interceptors. One approach: the auth interceptor catches the 401, triggers a token refresh, and relaunches the original request. It's more efficient than letting the retry interceptor blindly relaunches. The order of interceptor registration matters: auth first, then retry, then logging. Each interceptor should be independent yet aware of the others.

Testing Interceptors and Retry Strategies

Testing an interceptor without actually making network calls is possible with HttpClientTestingModule . You create a server mock with HttpTestingController , trigger requests, and observe retries. To validate exponential backoff, capture delays with fakeAsync and tick . Verify that jitter truly randomizes without exceeding limits. Also test edge cases: what if all attempts fail? If one attempt succeeds after 2 retries, is the response correct? Tests should cover both happy and failure paths and validate delays.

Operational Conclusion

HTTP interceptors with exponential retry backoff transform a fragile application into a resilient system. The implementation is not complex: just a few dozen lines of RxJS code. The impact is huge: absorb transient failures without application-specific code. Start simple (retry 5xx with backoff), monitor in production, and evolve toward a circuit breaker if needed. The key is testing failure paths, adding jitter, and not retrying blindly. A good retry strategy is invisible to the user but saves UX when the network or server falters.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️