← Salvatore Zappalà's Weblog
How Nx "pulled the rug" on us, a potential solution and lessons learned

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.
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):
- It doesn’t support bulk requests. Each artifact requires a single HTTP request to the cache. This ensures that a custom cache cannot compete with their commercial offering.
- In most cases, it requires managing and maintaining a remote cache service, which is a non-trivial task. However, we found a workaround for this.
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:
- CI job start: Your CI runner restores the
.portable-nx-cachedirectory from the CI cache (if it exists from previous runs) - The
portable-nx-cachebinary starts as a background process - Nx build commands communicate with the local cache server via HTTP
- Build artifacts are stored in
.portable-nx-cache/on the filesystem - When the job completes, your CI system saves the
.portable-nx-cachedirectory - Subsequent jobs restore the cache directory and have immediate access to all previous artifacts
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:
- As mentioned, it’s a single binary that can be dropped in your CI
- No external or bundled dependencies, just pure Go standard library
- High performance. Probably close to the maximum that can be extracted by the OpenAPI spec provided by Nx
- MIT licensed. Feel free to fork, edit, extend, etc.
- No maintenance ovehead. Set and forget, for as long as Nx will honor the OpenAPI spec, the binary will work
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.