# nestjs-cls: request context without threading it through every call

> nestjs-cls brings continuation-local storage to NestJS on top of Node's AsyncLocalStorage, so request scoped values such as a tenant connection or a transaction can be read from anywhere without REQUEST-scoped providers. It is a well maintained community package with a plugin ecosystem, and its own documentation admits that most of its use cases have clunky alternatives.

**Papooch/nestjs-cls** — A continuation-local storage (async context) module compatible with NestJS's dependency injection.

- Repository: https://github.com/Papooch/nestjs-cls
- Website: https://papooch.github.io/nestjs-cls/
- Stars: 707 · Forks: 54
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/papooch-nestjs-cls

## Context that survives the async call stack

nestjs-cls is a continuation-local storage module for NestJS, built on Node's AsyncLocalStorage. The description of what continuation-local storage does is the clearest way to understand the package: it lets you store state and propagate it throughout callbacks and promise chains, keeping data alive for the lifetime of a web request or any other asynchronous duration, and it is similar to thread-local storage in other languages.

That last point is the mental model most people carry over, and it is also where the trouble starts. In a threaded runtime a thread is a hard boundary, so a thread-local value simply is not there on another thread. In Node the equivalent boundary is the async execution path, and a value stored in one path does not leak into another. The reason the module is worth adding to a framework is that NestJS's own answer to the same problem, REQUEST-scoped providers, has costs that the package's documentation is upfront about.

The documentation's own framing is fair rather than promotional. It lists the use cases as tracking a request id and other metadata for logging, keeping track of the user through the whole request, making the dynamic tenant database connection available everywhere in multi-tenant apps, propagating an authentication level or role to restrict access, propagating a database transaction across services without breaking encapsulation and isolation by explicitly passing it around, and using request context in cases where REQUEST-scoped providers are not supported at all, listing passport strategies, cron controllers, websocket gateways and queue consumers.

That last group is the sharpest one. When a context is needed in a consumer or a cron job, there is no request to scope a provider to, and the alternative is to pass the value as an argument through a call chain that has nothing to do with the domain.

## A monorepo with plugins and transactional adapters

The repository is a Yarn workspace monorepo, not a single package, and the layout explains how the project is organised. The workspaces array in the root manifest lists packages/*, packages/transactional-adapters/* and docs, so the core module lives under packages, the adapter packages are a second tier of workspaces, and the documentation site is the third.

The plugins are the first tier of that structure. The README points at a Transactional plugin page and describes what it does as propagating a database transaction across services without breaking encapsulation and isolation by explicitly passing it around. It also points at a Proxy Providers page, presented as the way to replace REQUEST-scoped providers, which tells you the library's preferred answer to the framework's own mechanism is to substitute a different mechanism rather than to configure the existing one.

The root manifest also shows the tooling, which is a decent proxy for how the project is run. The test script fans out across all workspaces in topological order, and the build does the same while excluding the docs workspace. Formatting is prettier over the packages directory and linting is eslint over the same glob. There is a dependency-cruiser configuration and a depcruise script that renders the dependency graph to SVG and wraps it into HTML, so the architecture is checked as a graph and documented automatically.

The other package manager tooling is worth noting too: monoweave for versioning and the GitHub plugin for it, and a conventional changelog config, which together suggest that releases are generated from commit messages rather than hand-written.

## Node 22 and a Yarn 4 workspace root

The root manifest is a private package that exists to hold the workspace configuration, and it declares the requirements the whole project shares:

```json
  "engines": {
    "node": ">=22"
  },
  "packageManager": "yarn@4.12.0",
```

Node 22 as a floor is a meaningful constraint for anyone evaluating this for an existing service. It is not a package that will drop into a Node 18 codebase without a runtime upgrade first.

The Yarn version is pinned rather than ranged, which is the modern recommendation for reproducible installs and means the repository expects the Berry release of Yarn with its own resolution rules. The repository root carries a .yarnrc.yml and a .yarn directory, and the lockfile is yarn.lock.

Installation of the library itself is not described in the README, which is a deliberate choice rather than an omission. The documentation has been moved to a dedicated website, and the README says so explicitly at the top. The README is therefore a summary and a pointer, and the practical installation steps, the module registration, the middleware setup and the store configuration all live on that site.

The project identifies itself as a community project rather than an official NestJS package, with a CONTRIBUTING.md and a pull request policy that is described in the README as always welcome.

## Where the alternative approaches stop working

The README states plainly that most of the listed use cases are to some extent solvable with REQUEST-scoped providers or with passing the context as a parameter, and that those solutions are often clunky and come with a whole lot of other issues. Naming the alternatives is more useful than the claim, because each fails in a different way.

Passing the context as a parameter is the cheapest option and it always works. Its cost shows up in the shape of the code: every service method that needs the tenant or the transaction grows a parameter, and that parameter propagates into helpers that have no interest in it. In a deep call graph the context argument becomes the first thing every function takes, which is a design smell even when the value is needed only once.

REQUEST-scoped providers solve the propagation problem natively, since NestJS builds the injection scope into the container. The costs are the ones the library exists to avoid: a request-scoped provider makes everything that depends on it request-scoped too, so the whole subgraph is rebuilt per request, and request-scoped providers are not available in the places where a request does not exist, which is the passport strategy, cron controller, websocket gateway and queue consumer list from the README.

The library's claim is that continuation-local storage solves propagation without the per-request object graph churn and without the availability gaps. That is a specific, testable claim, and the way to evaluate it in your own application is to measure a request-scoped subgraph before and after, rather than to take the word of a library that exists to replace it.

## The transactional plugin and the boundary it does not cross

The Transactional plugin is the feature with the highest stakes, because it deals with a resource that must be either committed or rolled back as a unit. The README describes it as propagating a database transaction across services without breaking encapsulation and isolation by explicitly passing it around, which is the use case that made the library popular in the first place.

The repository layout confirms this is a first-class concern rather than a single plugin. The workspaces array includes packages/transactional-adapters/*, a whole family of packages, which in practice means the transaction is not tied to one ORM but exposed through adapters.

Two things follow from the design. First, the transaction must live in the same continuation-local store as everything else, so the store is per async execution path and a transaction started in one path is invisible to another. That is the correct behaviour for isolation, and it also means that work dispatched to a queue or scheduled outside the request path does not inherit the transaction, which is usually what you want but is worth knowing before you rely on it.

Second, using a transaction through the store removes the explicit argument passing, which is the point, but it also removes the type-level trace of where the transaction came from. A reader of a service method can no longer see that it expects to be inside a transaction. The library mitigates this with a proxy providers pattern, where the proxy is what makes the requirement visible at the call site, and the proxy is described as the recommended replacement for REQUEST-scoped providers. Read the proxy pattern documentation before you adopt the store, since that is where the ergonomics of the whole approach are decided.

## What the store does not carry between requests

There is a category of problem this library does not solve, and it is worth being clear about it because it is where people get surprised.

Continuation-local storage propagates within an async execution path. It does not create a global, and it does not persist anything beyond the lifetime of that path. If a value is set at the top of a request and the request ends, the value is gone with the request. If a piece of work is scheduled for later, it does not carry the value with it unless the value was captured at scheduling time inside that path.

That distinction is what makes the library safe in a server context, where a global would be a data leak waiting to happen, and it is also the reason certain patterns need help. The README's own list of hard cases points in this direction, since passport strategies, cron controllers, websocket gateways and queue consumers are exactly the places where no request scope exists to inherit from. The proxy provider mechanism exists precisely to inject the store into those entry points, so the value is set explicitly where the work begins rather than inherited from a caller that does not exist.

The other boundary is horizontal. Two concurrent requests each get their own store, so a tenant identifier set in one request cannot be read by another. If your application has a background worker that legitimately needs the tenant of the job it is processing, that tenant has to come from the job payload, and setting it into the store at the top of the worker is the pattern the design expects.

## When plain NestJS is the right answer

The alternative worth naming is not another library, it is the framework you already have. NestJS's request scope, used sparingly, and plain parameter passing will solve most of the problems people bring to this package, and they come with the advantage that the context is visible in the code.

There are cases where that is not enough. A codebase that needs the tenant connection available in twenty services that have no relationship to each other, or that needs a transaction spanning service boundaries without every method carrying it, is a codebase where parameter passing becomes the dominant shape of every interface. In that situation the library genuinely removes something rather than adding a layer.

There are also cases where it is the wrong tool for a different reason. If your application is not built on NestJS, the module is irrelevant. If the values you need are process-wide configuration rather than per-request state, a configuration service is clearer. And if you are on Node 18, the runtime floor the project declares means the decision is not yours to make yet.

The honest framing is that this package trades explicitness for convenience in a specific, identifiable place. It is a good trade when the number of consumers of the context is large and the number of entry points is small. It is a bad trade when a single service needs one value, because then the parameter was free and the store is overhead.

## Conclusion

Adopt nestjs-cls when a NestJS application has to carry request scoped state across service boundaries, especially multi-tenant database connections and transactions, and when REQUEST-scoped providers have already caused you problems with performance or context. Do not adopt it for a service with no shared request state, where the injection overhead buys nothing, and do not adopt it as a way to avoid thinking about context boundaries, since the store is still per async execution path. Before you commit, read the plugin list for the specific adapters you use, check that your Node version satisfies the >=22 requirement the workspace declares, and confirm that your transaction or ORM adapter is covered by the transactional adapters packages rather than assuming the generic API covers it.

## FAQ

### What is nestjs-cls?

It is a continuation-local storage module for NestJS built on Node's AsyncLocalStorage, letting a service read request scoped state such as a request id, the current user, a tenant database connection or an open transaction without it being passed as a parameter. It is MIT licensed and maintained as a community project.

### What Node.js version does nestjs-cls require?

The workspace root declares the engines field with node set to >=22, and pins Yarn 4.12.0 as the package manager. The repository is a Yarn workspace monorepo with packages, transactional adapter packages and docs as separate workspaces.

### How does nestjs-cls differ from REQUEST-scoped providers in NestJS?

The README states that most use cases are to some extent solvable with REQUEST-scoped providers or by passing the context as a parameter, but that those solutions are often clunky and come with other issues. REQUEST-scoped providers also are not available in places such as passport strategies, cron controllers, websocket gateways and queue consumers, which the library lists as cases where request context is needed anyway.

### What does the Transactional plugin in nestjs-cls do?

It propagates a database transaction across services without breaking encapsulation and isolation by explicitly passing it around. The repository carries a family of packages under packages/transactional-adapters, so the transaction is exposed through adapters rather than tied to one ORM.

### Where is the documentation for nestjs-cls?

The documentation has been moved to a dedicated website at papooch.github.io/nestjs-cls, and the README says so at the top. The repository also keeps a docs/ workspace in the Yarn workspace list, and the README links to pages for the available plugins, proxy providers and other features.

## Sources

- [License: MIT](https://github.com/Papooch/nestjs-cls/blob/main/LICENSE)
- [Papooch/nestjs-cls on GitHub](https://github.com/Papooch/nestjs-cls)
- [Project website](https://papooch.github.io/nestjs-cls/)
- [README](https://github.com/Papooch/nestjs-cls/blob/main/README.md)
- [Releases](https://github.com/Papooch/nestjs-cls/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/papooch-nestjs-cls
