CI has become the bottleneck. What was initially just an issue for large-scale enterprise monorepos is now affecting everyone. Coding agents push far more often than humans do (Anthropic reported 25x more CI jobs in six months), and every push starts a full CI run.
Trying to fix this by switching to providers that promise faster, more powerful machines is not gonna help in the long run.
The only way to fix it is to use a deep understanding of your codebase to run CI more efficiently, and to run less of it. Here's how Nx Cloud CI does it.
Critical-path scheduling
Running CI means running a series of tasks. Some of these tasks are independent, like linting or unit tests, while others form chains: a shared package build needs to run before the specific UI or application build, which in turn needs to run before an application e2e test can run.
Most CI setups don't care and just process tasks in the order they come in. Nx Cloud CI, on the other hand, knows exactly which tasks are about to run, how they relate to each other, and how long each task took in earlier runs.
This information allows it to schedule tasks in a highly optimized way. Tasks that unblock future tasks run first, and so do long-running tasks that are hard to distribute. Other, smaller independent tasks fill in the gaps and thus help improve overall compute utilization.
Dynamic task packing
Having an efficient scheduling algorithm is the first part of a smart CI setup. The next question is how you pack these tasks onto multiple machines while optimizing for throughput and utilization.
Why is high compute utilization important? Because you pay for VM-minutes, not for used compute. A VM-minute costs the same whether the machine is at 100% CPU or waiting on a single slow task.
Hence, if you can pack more compute into the same or even fewer VM-minutes, you save money while having a faster CI run!
Nx Cloud CI records the CPU and memory profile of every task as it runs. When a pipeline runs and a worker has free capacity, it gets assigned the task that fits that compute budget.
A lint task that needs one core can run next to a build that needs four. A memory-intensive e2e suite, instead, lands on its own machine.
In a more traditional, static CI config you'd have to decide on the parallelism up front and have a fixed matrix of which task goes on which machine. That has to account for the worst-case scenario because you have to make sure you don't go over compute capacity. This leads to low utilization during your average run.
Nx Cloud CI with dynamic task packing, on the other hand, does this at run time based on data from previous CI runs. This leads to much denser task scheduling and thus higher utilization.
Dynamic worker scaling
Packing makes every machine count. The next question is how many machines a run needs. Your CI should never run all the tasks on every PR. That's almost certainly a waste of compute. If you use something like task-based affected, you'll have a vastly different number of tasks to process for each PR.
So when a PR only touches a handful of projects, you might end up with only a few tasks to run that can easily be handled by one or two machines, vs. the 10 you might need in a worst-case scenario.
Nx Cloud CI provides a feature to dynamically scale workers based on how many tasks need to be processed.
jobs:
main:
workers:
small: 3
medium: 6
large: 8
steps:
- run: ...You define how many workers to use for small, medium and large changesets. Since Nx Cloud CI knows which tasks are affected and which are already cached, it can then start the corresponding set of workers to process the tasks that need to run.
Ultracache
So far, this was about running CI more efficiently. The biggest lever to make CI faster and save costs, though, is to run less. What doesn't need to run, shouldn't run. Remote caching in combination with task-based affected plays a major role here.
To apply caching to tasks, you need to know the inputs and outputs of that task. Most build tools either have you define these manually or statically infer them from the project graph. This is suboptimal, though, as they might be too coarse-grained and thus invalidate the cache when they shouldn't (leading to fewer cache hits) or be entirely wrong, leading to false positives.
Nx Cloud CI uses Ultracache, which doesn't use a static configuration but rather records which files each task reads and writes while it runs on CI. These observed reads and writes become cache inputs and outputs.
The result of using Ultracache:
- correct and always up-to-date cache configuration
- higher cache-hit rates
Fine-grained e2e test splitting (Atomization)
E2e tests have gained quite some popularity and importance with AI agents. First, they verify actual user behavior, which makes them a valuable indicator of whether things still work the way they should. Second, AI agents are really good at writing them.
There's just one problem: e2e tests tend to be extremely slow and become a major bottleneck in CI.
The techniques mentioned so far, such as dynamic task packing and Ultracache, are the building blocks for a smarter, faster CI. But they can't help as much if your e2e tests run as one single large task:
- Dynamic task packing can't spread it across machines and has a hard time scheduling other tasks around it, which leads to much poorer machine utilization overall.
- With simple remote caching it's all or nothing. If you hit the cache, you're lucky. If not, the entire e2e suite runs again.
Nx Cloud CI solves this with fine-grained e2e test splitting, which we call Atomization.
Atomization splits large test targets into one task per test file. The Nx plugins for Playwright, Cypress, Jest, Vitest and Gradle do this automatically. Once you have fine-grained tasks, the mechanisms we discussed earlier can schedule them efficiently.
If you combine this with Ultracache, you even get fine-grained caching at the single spec level. All it takes is to let your agent add a directive to the e2e tests.
Benchmarks
We run the same monorepo benchmark on Nx Cloud CI and on GitHub Actions with Nx remote caching. That baseline is already an optimized pipeline. The benchmark measures a feature change, a large change and a full rebuild, weighted by how often each happens in a typical workspace.
Nx Cloud CI finishes 10.6x faster and uses 79% less compute than GitHub Actions.
Customer workspaces show the same kind of savings. In workspaces whose CI was already heavily tuned, dynamic task packing alone cut VM-minutes by 35 to 50%.
Putting it together
None of these techniques needs a faster machine. Critical-path scheduling, dynamic task packing and dynamic worker scaling run your CI more efficiently. They get more out of every VM-minute and start only the machines a PR needs. Ultracache and Atomization let you run less, because only the work a change affects actually runs.
Nx Cloud CI is currently in private beta. Sign up to get early access.








