Recently our team ran into a production incident caused by a subtle interaction in NestJS’s dependency injection system.

The root cause was a request-scoped provider being injected into a service that was also used by a background worker. At first glance the code looked perfectly valid, but the runtime behavior was very different from what we expected.

This article explains:

  • what happened
  • why it happens in NestJS
  • how dependency scope propagation works
  • how to design services to avoid this class of issue

The Incident

We had a service that was intended to run as a background worker, such as a cron job or queue consumer. Later, someone added a dependency on a request-scoped service.

@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
  constructor(@Inject(REQUEST) private readonly req: Request) {}

  getUserId() {
    return this.req.user?.id;
  }
}

Then somewhere else:

@Injectable()
export class WorkerService {
  constructor(private readonly ctx: RequestContextService) {}
}

After deployment, the worker started failing or not executing correctly. Nothing in the code looked obviously wrong. The issue came from how NestJS propagates provider scopes.

How NestJS Dependency Injection Normally Works

By default, NestJS providers are singletons. Only one instance exists for the entire application lifecycle. The dependency graph is built once at application startup and reused.

@Injectable()
export class OrderService {}

In this case:

  • all providers are singletons
  • the graph is created once
  • requests reuse the same instances

This is efficient and predictable.

Request Scope in NestJS

NestJS also supports request-scoped providers. A new instance is created for each HTTP request. Nest creates a request-specific DI container and destroys it after the request finishes.

@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {}

The lifecycle looks like this:

HTTP request -> create request DI container -> instantiate request-scoped providers -> handle request -> destroy container

That model works well when a provider really needs request-bound state.

The Important Rule: Scope Cascades Upward

A behavior many developers miss is that scope is not isolated to the provider where you declare it.

If a singleton provider injects a request-scoped provider, NestJS promotes the parent provider to request scope as well.

For example:

SingletonService -> RequestScopedService

In practice, the singleton is no longer truly singleton. NestJS needs to resolve it from a request-specific context, so the parent becomes request-scoped too. This promotion can continue upward through the dependency graph.

That is the key detail behind this class of bugs.

Why This Breaks Background Workers

Background workers usually run outside the HTTP request lifecycle.

Typical examples:

  • cron jobs
  • queue consumers
  • scheduled tasks
  • startup jobs

These executions do not have an HTTP request context.

When NestJS tries to resolve a request-scoped provider, it needs a request container. But for a worker there is no request. Once a request-scoped dependency enters that graph, the worker service may no longer be safely resolvable in the way you expected.

Possible outcomes include:

  • runtime errors
  • dependency resolution failures
  • background tasks silently not running as expected

The exact symptom depends on how the worker is wired and when resolution happens, but the root cause is the same: request scope leaked into code that assumes an application-level lifecycle.

Why This Feels Different in Spring

Developers coming from Spring often expect this to behave differently.

In Spring, request-scoped dependencies are commonly injected through scoped proxies. That means a singleton bean can still hold a proxy reference, and the actual request-bound object is resolved later against the current execution context.

NestJS uses a simpler model. If you depend on a request-scoped provider, the dependent provider itself becomes request-scoped.

That design is consistent, but it also means scope changes can propagate farther than you expect.

Safer Architecture Patterns

1. Avoid Injecting Request Context Into Core Services

Keep business services stateless when possible.

class OrderService {
  createOrder(userId: string) {}
}

Controllers can extract request metadata and pass it explicitly.

That keeps core services reusable across:

  • HTTP handlers
  • workers
  • scheduled jobs
  • tests

2. Pass Context Explicitly to Workers

Workers should receive the context they need through the job payload itself.

type JobPayload = {
  tenantId: string;
  correlationId: string;
  data: any;
};

This makes worker execution independent from HTTP infrastructure and much easier to reason about.

3. Use AsyncLocalStorage for Cross-Cutting Request Metadata

If you need global-style access to request metadata such as request ID, tenant ID, or trace data, AsyncLocalStorage is usually a safer fit than request-scoped DI for shared infrastructure concerns.

Conceptually:

HTTP request -> AsyncLocalStorage context -> singleton services read contextual metadata

This is especially useful for:

  • logging
  • tracing
  • correlation IDs

It is still important to treat it as contextual metadata, not as a substitute for explicit business inputs.

Key Lessons

  1. NestJS request scope cascades through dependencies.
  2. Injecting a request-scoped provider can silently change a service’s lifecycle.
  3. Background workers should avoid request-scoped dependencies.
  4. Request context is usually better handled with explicit parameters, AsyncLocalStorage, or job payload metadata.

Production Checklist

When building NestJS systems that include both APIs and background workers:

  • avoid injecting request-scoped providers into worker services
  • keep business logic services singleton and stateless where possible
  • pass request metadata explicitly from controllers
  • include correlation metadata in job payloads
  • use AsyncLocalStorage for logging and tracing concerns
  • review scope propagation whenever adding new dependencies to shared services

Final Thoughts

NestJS’s dependency injection system is elegant and simple, but some behaviors are easy to miss until they fail in production.

Understanding how provider scopes propagate matters when you design systems that combine HTTP APIs, background workers, queues, and scheduled jobs.

A small dependency change can unexpectedly alter runtime behavior. Being deliberate about where request context lives helps avoid that entire class of surprise.