NestJS Enterprise Backend APIs · Ders

Interceptor'larla Zaman Aşımları, Yeniden Denemeler ve Yalıtım

RxJS tabanlı zaman aşımı ve yeniden deneme interceptor'larının yanı sıra dış çağrılar için eşzamanlılık yalıtımları uygulayın.

1. ders / 413 adım

Interceptor'larla Zaman Aşımları, Yeniden Denemeler ve Yalıtım, CoddyKit'te ücretsiz bir NestJS Enterprise Backend APIs dersidir. Bu, 4 dersinin 1. dersidir. Aşağıdan dersin tamamını ücretsiz okuyabilir, sonra tarayıcıda yerleşik kod editörü ve 7/24 yapay zeka koçu ile uygulamalı olarak pratik yapabilirsin. Bu, NestJS Enterprise Backend APIs öğrenme yolunun bir parçasıdır ve ilerlemeniz web ve CoddyKit uygulaması arasında senkronize olur. NestJS Enterprise Backend APIs kursu toplamda 4 dersten oluşur.

Bu dersin bazı bölümleri henüz çevrilmemiş olup İngilizce olarak gösterilmektedir.

Why Resilience Patterns Belong in Interceptors

Outbound calls to other services fail in three classic ways: they hang forever, they fail transiently, or they flood a slow dependency until it (and you) collapse. The three matching defenses are timeouts, retries, and bulkheads.

  • Timeout — cap how long any one call may run.
  • Retry — re-attempt a failed call a bounded number of times, ideally only for transient errors.
  • Bulkhead — cap how many calls run concurrently so one dependency can't exhaust your resources.

In NestJS these compose cleanly as NestInterceptors. An interceptor wraps the handler's RxJS stream, so we can layer timeout and retry operators on the response Observable without touching business logic.

The Interceptor Contract

A NestJS interceptor implements intercept(context, next) and returns an Observable. Calling next.handle() runs the route handler and gives you its result stream. Anything you pipe onto that stream — timeout, retry, catchError — applies to the response.

  • This is why resilience logic lives here: it is cross-cutting and stream-based.
  • Operators run in order, so placement of timeout vs retry changes behavior — we will exploit that.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    const started = Date.now();
    // next.handle() executes the route handler; we pipe onto its stream.
    return next.handle().pipe(
      tap(() => console.log(`took ${Date.now() - started}ms`)),
    );
  }
}

A Timeout Interceptor

The RxJS timeout operator emits a TimeoutError if the source does not emit within the given window. We catch that error and convert it into a proper HTTP response — 504 Gateway Timeout — instead of leaking an RxJS error class.

  • Always translate TimeoutError into a meaningful status; an unhandled one becomes a generic 500.
  • Re-throw anything that is not a timeout so other filters can handle it.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, RequestTimeoutException } from '@nestjs/common';
import { Observable, TimeoutError, throwError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  constructor(private readonly ms = 5000) {}

  intercept(_ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle().pipe(
      timeout(this.ms),
      catchError((err) =>
        err instanceof TimeoutError
          ? throwError(() => new RequestTimeoutException('Upstream call timed out'))
          : throwError(() => err),
      ),
    );
  }
}

Making the Timeout Configurable Per Route

A flat 5s timeout rarely fits every endpoint. Expose the value via metadata so each handler can override it. Read it with Reflector, falling back to a default.

  • Define a @Timeout(ms) decorator using SetMetadata.
  • The interceptor pulls the value off the handler with reflector.get.

This keeps the policy declarative: the route says how patient it is.

import { SetMetadata, applyDecorators } from '@nestjs/common';

export const TIMEOUT_MS = 'timeout_ms';
export const Timeout = (ms: number) => applyDecorators(SetMetadata(TIMEOUT_MS, ms));

// In the interceptor:
// const ms = this.reflector.get<number>(TIMEOUT_MS, ctx.getHandler()) ?? 5000;
// return next.handle().pipe(timeout(ms), /* catchError ... */);

Retrying Transient Failures

RxJS retry resubscribes to the source when it errors. Naive retry(3) hammers a struggling dependency. Use retry({ count, delay }) where delay is a function returning an Observable — this lets us add exponential backoff with jitter and filter which errors are retryable.

  • Retry only idempotent operations (GET, PUT, DELETE) — never blind-retry a POST that may have already succeeded.
  • Retry only transient errors: timeouts, 502/503/504, connection resets — not a 400 or 422.
import { Observable, throwError, timer } from 'rxjs';
import { retry } from 'rxjs/operators';

const RETRYABLE = new Set([502, 503, 504]);

function withRetry<T>(source: Observable<T>): Observable<T> {
  return source.pipe(
    retry({
      count: 3,
      delay: (err, attempt) => {
        const status = err?.response?.status;
        if (status && !RETRYABLE.has(status)) return throwError(() => err);
        const base = 100 * 2 ** (attempt - 1); // 100, 200, 400 ms
        const jitter = Math.random() * base;
        return timer(base + jitter);
      },
    }),
  );
}

Backoff Math, Standalone

Before wiring backoff into an interceptor, it helps to see the delays. Full jitter picks a random delay in [0, base] to spread retries and avoid the thundering herd where every client retries at the same instant.

This snippet just prints the schedule — pure TypeScript, no framework.

function backoffSchedule(maxAttempts: number, baseMs: number): number[] {
  const delays: number[] = [];
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const cap = baseMs * 2 ** (attempt - 1);
    const fullJitter = Math.floor(Math.random() * cap); // [0, cap)
    delays.push(fullJitter);
  }
  return delays;
}

const schedule = backoffSchedule(5, 100);
console.log('Caps:   ', [100, 200, 400, 800, 1600].join(', '));
console.log('Jittered:', schedule.join(', '));
console.log('Total wait:', schedule.reduce((a, b) => a + b, 0), 'ms');

Ordering: Timeout Inside, Retry Outside

Operator order is the subtle part. Put timeout before retry in the pipe so each attempt gets its own deadline; retry then resubscribes and re-arms the timeout for the next attempt.

  • timeout then retry — per-attempt deadline, N fresh tries. Usually what you want.
  • retry then timeout — one deadline spanning all attempts; a slow first try eats the whole budget.

Always bound the worst case: attempts × (timeout + maxBackoff) must stay under the caller's own deadline.

import { CallHandler, ExecutionContext, Injectable, NestInterceptor, RequestTimeoutException } from '@nestjs/common';
import { Observable, TimeoutError, throwError, timer } from 'rxjs';
import { catchError, retry, timeout } from 'rxjs/operators';

@Injectable()
export class ResilientInterceptor implements NestInterceptor {
  intercept(_ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle().pipe(
      timeout(2000),               // per-attempt deadline
      retry({ count: 3, delay: (_e, n) => timer(100 * 2 ** (n - 1)) }),
      catchError((err) =>
        err instanceof TimeoutError
          ? throwError(() => new RequestTimeoutException())
          : throwError(() => err),
      ),
    );
  }
}

The Bulkhead: Capping Concurrency

Timeouts and retries protect a single request. A bulkhead protects the whole process: it limits how many in-flight calls a dependency may have, so a slow downstream can't pile up unbounded promises and exhaust threads, sockets, or memory.

  • Calls beyond the limit either queue (bounded) or are rejected fast (503).
  • Named after ship compartments: a flood in one section is sealed off from the rest.

A minimal bulkhead is a semaphore: a permit count plus a waiter queue.

export class Bulkhead {
  private active = 0;
  private readonly queue: Array<() => void> = [];

  constructor(private readonly maxConcurrent: number, private readonly maxQueue: number) {}

  async run<T>(task: () => Promise<T>): Promise<T> {
    if (this.active >= this.maxConcurrent) {
      if (this.queue.length >= this.maxQueue) throw new Error('BULKHEAD_FULL');
      await new Promise<void>((resolve) => this.queue.push(resolve));
    }
    this.active++;
    try {
      return await task();
    } finally {
      this.active--;
      this.queue.shift()?.();
    }
  }
}

A Runnable Bulkhead Simulation

Here is the bulkhead exercised end-to-end with simulated async work. Watch that no more than maxConcurrent tasks run at once, and overflow past the queue is rejected immediately — the fast-fail that keeps your service healthy.

class Bulkhead {
  private active = 0;
  private queue: Array<() => void> = [];
  constructor(private max: number, private maxQueue: number) {}
  async run<T>(task: () => Promise<T>): Promise<T> {
    if (this.active >= this.max) {
      if (this.queue.length >= this.maxQueue) throw new Error('BULKHEAD_FULL');
      await new Promise<void>((r) => this.queue.push(r));
    }
    this.active++;
    try { return await task(); }
    finally { this.active--; this.queue.shift()?.(); }
  }
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const bh = new Bulkhead(2, 1);
let peak = 0, running = 0;

async function call(id: number) {
  try {
    await bh.run(async () => {
      running++; peak = Math.max(peak, running);
      await sleep(50);
      running--;
    });
    return `ok-${id}`;
  } catch (e) { return `rejected-${id}`; }
}

async function main() {
  const results = await Promise.all([1, 2, 3, 4, 5].map(call));
  console.log(results.join(', '));
  console.log('peak concurrency:', peak);
}
main();

Wiring the Bulkhead into an Interceptor

To apply a bulkhead per outbound dependency, hold a Bulkhead instance in a provider and run the handler stream through it. Use from(promise) to bridge the async run() back into RxJS, then layer timeout and retry on top.

  • One bulkhead instance per dependency, not per request — the limit is shared, so it must be a singleton provider.
  • Convert BULKHEAD_FULL into 503 Service Unavailable so callers can back off.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, ServiceUnavailableException } from '@nestjs/common';
import { Observable, defer, lastValueFrom } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Bulkhead } from './bulkhead';

@Injectable()
export class BulkheadInterceptor implements NestInterceptor {
  private readonly bulkhead = new Bulkhead(10, 20); // shared, singleton-scoped

  intercept(_ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    return defer(() => this.bulkhead.run(() => lastValueFrom(next.handle()))).pipe(
      catchError((err) => {
        if (err?.message === 'BULKHEAD_FULL') throw new ServiceUnavailableException('Capacity reached');
        throw err;
      }),
    );
  }
}

Composing All Three at the HttpModule Layer

In practice you rarely intercept inbound routes for outbound resilience — you wrap the HTTP client. With Nest's HttpService (Axios + RxJS) you pipe the same operators onto each outbound observable, and run it through the dependency's bulkhead.

  • Tune budgets per dependency: a fast cache gets a 200ms timeout; a report API gets 10s and zero retries.
  • Pair this with a circuit breaker so a dependency that keeps failing is shed before timeouts even fire.
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { AxiosResponse } from 'axios';
import { Observable, defer, lastValueFrom, throwError, timer } from 'rxjs';
import { retry, timeout } from 'rxjs/operators';
import { Bulkhead } from './bulkhead';

@Injectable()
export class PricingClient {
  private readonly bulkhead = new Bulkhead(8, 16);
  constructor(private readonly http: HttpService) {}

  getPrice(sku: string): Observable<AxiosResponse> {
    return defer(() =>
      this.bulkhead.run(() =>
        lastValueFrom(
          this.http.get(`/pricing/${sku}`).pipe(
            timeout(800),
            retry({ count: 2, delay: (_e, n) => timer(100 * 2 ** (n - 1)) }),
          ),
        ),
      ),
    );
  }
}

Quick Check

You wrap an outbound call with both a 2s timeout and a 3-attempt retry inside one RxJS pipe. You want each attempt to have its own 2-second deadline. Which operator ordering achieves that?

Recap

You built the three core outbound-resilience patterns as composable NestJS interceptors:

  • Timeout — timeout(ms) + catchError to translate TimeoutError into 504/408; make it per-route via Reflector metadata.
  • Retry — retry({ count, delay }) with exponential backoff and full jitter, restricted to idempotent operations and transient status codes.
  • Bulkhead — a semaphore (active count + bounded queue) per dependency that fast-fails with 503 when capacity is reached, isolating one slow downstream.

Key decisions: place timeout before retry for per-attempt deadlines; keep bulkheads singleton-scoped per dependency; and always bound the worst case (attempts × (timeout + backoff)) below the caller's deadline. Combine these with a circuit breaker for full steady-state protection.

Başlamak ücretsiz

Yapay zeka eğitmeniyle TypeScript öğren — ücretsiz

Tarayıcında gerçek kod yaz ve çalıştır, 7/24 yapay zeka eğitmeninden anında yardım al; web'de ya da uygulamada kaldığın yerden devam et.

Kurslar
20
Dersler
76

Sıkça Sorulan Sorular

“Interceptor'larla Zaman Aşımları, Yeniden Denemeler ve Yalıtım” dersi ücretsiz mi?

Evet — “Interceptor'larla Zaman Aşımları, Yeniden Denemeler ve Yalıtım” dersin tüm metni burada web'de ücretsiz olarak okunabilir. Etkileşimli olarak pratik yapmak (yerleşik kod editörü ve 7/24 yapay zeka koçu) ve NestJS Enterprise Backend APIs kursunun geri kalanını açmak için CoddyKit PRO'ya yükselt. NestJS Enterprise Backend APIs kursu toplamda 4 dersten oluşur.

“Interceptor'larla Zaman Aşımları, Yeniden Denemeler ve Yalıtım” dersinde ne öğreneceğim?

RxJS tabanlı zaman aşımı ve yeniden deneme interceptor'larının yanı sıra dış çağrılar için eşzamanlılık yalıtımları uygulayın. NestJS Enterprise Backend APIs ile uygulamalı kodu tarayıcıda doğrudan çalıştırarak pratik yaparsın ve 7/24 yapay zeka koçu dersi çalışırken sorularını yanıtlar.

NestJS Enterprise Backend APIs öğrenmeye başlamak için deneyim gerekli mi?

Önceden deneyim gerekmez. CoddyKit'te NestJS Enterprise Backend APIs, başlangıçtan ileri seviyeye kadar yapılandırıldığı için buradan başlayabilir veya başından başlayıp kendi hızında ilerleme yapabilirsin. Bu, 4 dersinin 1. dersidir.

“Interceptor'larla Zaman Aşımları, Yeniden Denemeler ve Yalıtım” dersi ne kadar sürer?

Çoğu CoddyKit dersi yaklaşık 5–10 dakika sürer. Her biri kısa ve etkileşimli olduğu için sabit ilerleme yaparsın ve web ile uygulama arasında tam olarak bıraktığın yerden devam edebilirsin.

Bu NestJS Enterprise Backend APIs dersinde kod yazıp çalıştırabilir miyim?

Evet. Her NestJS Enterprise Backend APIs dersi yerleşik bir kod editörü içerir, bu sayede tarayıcıda gerçek kod yazıp çalıştırabilir ve anlık yapay zeka geri bildirimi alırsın — yerel kurulum gerekli değildir.

Bu kursun tüm dersleri

  1. Interceptor'larla Zaman Aşımları, Yeniden Denemeler ve Yalıtım
  2. Aşağı Akış Hataları için Devre Kesiciler
  3. OpenTelemetry ile Dağıtık İzleme
  4. SLO'ları ve Hata Bütçelerini Tanımlama
← NestJS Enterprise Backend APIs Sayfasına Dön