How Nx "pulled the rug" on us, a potential solution and lessons learned

Portable Nx Cache

TL;DR: Nx deprecated custom task runners. I built portable-nx-cache, a MIT licensed drop-in Go binary that provides remote caching via your CI’s filesystem cache.

One of my responsibilities at my current job is maintaining a decently-sized monorepo housing hundreds of Node.js libraries and applications. For a repository of this size, it’s handy to have a set of build tools that support, among other things, remote caching and remote execution. This repository has been running on Nx for more than 5 years now.

Overall, we’ve been quite happy with the developer experience, and even though it feels a bit overcomplicated at times, it has really helped us manage our environment.

Nrwl (Narwhal Technologies), the company behind Nx, is a Series A-funded startup, and their business model is to funnel users into signing up for their Nx Cloud services. Nx Cloud offers a good set of “cloud” features on top of the “free” Nx, such as advanced remote caching and remote execution, “self-healing CI,” among others.

Try the full Nx platform prompt Nx installation prompts users to try their commercial platform

Teams like ours that only needed remote caching – without all the additional features provided by Nx Cloud – had the option to implement custom task runners that would provide the required caching mechanism. An example of such an implementation is nx-remotecache-custom on npm.

Unfortunately, in 2024, the creators of Nx — perhaps under pressure to become more profitable — deprecated support for custom task runners, causing quite a bit of controversy on a few GitHub issues, i.e. #28150 and #30548.

To replace custom runners, they provided Nx Powerpack, which, while free, requires a commercial license. In our case, this means going through procurement, which is a no-go for us. Procurement is a time and energy consuming process that, to be honest, is not worth the hassle just to have some remote caching in CI.

After the community backlash, they did provide an OpenAPI specification so that users can implement their own remote cache. However, this specification has two shortcomings (IMHO, by design):

After thinking about this problem for a while, we found a way to leverage the GitLab CI filesystem cache and the Nx OpenAPI specification such that there is no need to manage a full-blown service.

While the original implementation of this caching system I built at my company is proprietary, I developed an open-source variant and published it on GitHub under the MIT license.

In essence, this is a portable drop-in binary built in Go that implements the new OpenAPI specification provided by the creators of Nx. It stores the cached artifacts in the filesystem, which allows you to leverage your CI caching mechanism, such as GitLab CI cache.

The workflow is straightforward:

  1. CI job start: Your CI runner restores the .portable-nx-cache directory from the CI cache (if it exists from previous runs)
  2. The portable-nx-cache binary starts as a background process
  3. Nx build commands communicate with the local cache server via HTTP
  4. Build artifacts are stored in .portable-nx-cache/ on the filesystem
  5. When the job completes, your CI system saves the .portable-nx-cache directory
  6. Subsequent jobs restore the cache directory and have immediate access to all previous artifacts

Portable Nx Cache Architecture Architecture diagram showing how portable-nx-cache integrates with GitLab CI

For example, the following configuration works on GitLab:

#################################################
# Portable Nx Cache Setup
#################################################

variables:
  PORTABLE_NX_CACHE_PORT: "8080"
  # This token is required by Nx and must match NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN
  PORTABLE_NX_CACHE_TOKEN: "portable-nx-cache"
  NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: "portable-nx-cache"
  NX_SELF_HOSTED_REMOTE_CACHE_SERVER: "http://localhost:$PORTABLE_NX_CACHE_PORT"


.portable-nx-cache:
  before_script:
    # Assumes the portable-nx-cache binary is already present in your repository
    # (e.g., committed to ./tools/portable-nx-cache or added via a setup step)
    - ./tools/portable-nx-cache > portable-nx-cache.log 2>&1 &
    # Wait for the cache service to be ready before continuing
    - sh -c "until curl --output /dev/null --silent --head --fail http://localhost:$PORTABLE_NX_CACHE_PORT/ready; do sleep 1; done"
  artifacts:
    paths:
      - portable-nx-cache.log
  cache:
    - key: portable-nx-cache
      paths:
        - .portable-nx-cache


#################################################
# Example of Nx command using the cache
#################################################

build:
  stage: build
  extends:
    - .portable-nx-cache
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
  script:
    # The cache URL and Token are now auto-injected from 'variables', so Nx will
    # use the Portable Nx Cache server locally
    - nx affected --base=$BASE_SHA --head=$HEAD_SHA --target=build

Some notable features:

The project is available on GitHub: https://github.com/salvozappa/portable-nx-cache

Feel free to use it as you wish. I’ve decided not to provide pre-built binaries, but building is straightforward with a simple make build command.

This is a project I built in my free time, so expect no warranties and no official support. That said, constructive feedback and pull requests are welcome.

I understand the reasoning behind Nx guys’ decisions. Making an open-source company profitable is genuinely difficult, and businesses need sustainable revenue models. However, deprecating custom task runners, a functionality that teams relied on, felt like pulling the rug out from under users. The fact that they later backtracked by providing the OpenAPI spec might indicate they recognised it.

For me, the lesson learned is the following: when choosing tools in a corporate environment where procurement is non-trivial, I must evaluate the vendor’s business model. Tools that depend on converting free users to paying customers carry risks. Features one depends on today may be paywalled tomorrow, and organizational friction (like procurement processes) can make adapting to those changes unfeasible.

If you found yourself in this specific scenario, portable-nx-cache might suit you. It doesn’t have much bells and whistles, but it’s a more than decent way to have no-fuss remote cache in CI.

If it helps you, consider giving it a star on GitHub or sharing it with others facing the same challenges.

Salvatore Zappalà