‹ Blog
Juri Strumpflohner
Juri StrumpflohnerJuri Strumpflohner

Introducing dynamic task packing and Ultracache

Coding agents produce more changes than CI was built to validate. Anthropic went from about 2,500 merged changes a day in May to more than 30,000 in September, and in six months their tests grew 10x and their CI jobs 25x. At Linear, the test suite nearly quadrupled since January, with agents writing most of the roughly 2,000 new tests per week.

Throwing faster machines at slow CI works, and it is the easy fix. But as your codebase grows, so does your compute bill.

What you need instead is CI that schedules and caches tasks based on real execution data:

  • Dynamic task packing - distributes tasks across any number of machines and packs each one close to full capacity.
  • Ultracache - a fully automatic computation cache that learns task inputs and outputs from the actual runs.

Both are available for everyone starting today.

VM minutes and machine utilization

When you size a CI machine, you want to run as much in parallel as possible without making the run unstable. Every run has peaks, and those peaks decide how much you can run in parallel and how big the machine needs to be.

CI machine utilization over a 10 minute run: two short peaks exceed the provisioned capacity, while average utilization is 20% and most of the paid capacity sits idle

The peaks are not evenly distributed, so most of the time the machine runs at low utilization. A faster machine improves CI times overall, but it does not help with utilization. You end up paying for fast machines that mostly sit idle.

The thing to understand here is that you don't pay for compute, you pay for VM minutes. So the goal is to pack as much compute as possible into the VM minutes you already pay for.

The same CI work packed onto one right-sized machine: the run finishes in under 4 minutes, 6.2 minutes faster, with average utilization at 52%

To do that well, you need to know the resource profile of every task and how it behaves in CI: how much memory it uses, how its CPU usage is distributed over the run, and how all of that behaves across multiple machines.

Dynamic task packing

Most distribution setups assign tasks to machines up front, often by type. End-to-end tests go to one machine, unit tests to another and builds to a third. That setup is static. It doesn't adapt when the codebase changes, and it can't react when one PR runs 10 tasks and the next one runs 200.

Dynamic task packing instead looks at the CI run as a whole:

  1. It collects all tasks that need to run for that CI run.
  2. It analyzes their resource profile based on historical runs (how much CPU and memory each task needs).
  3. It packs them onto CI workers based on how they fit and how much capacity each worker has at any given moment.
  4. It prioritizes tasks by their dependencies. A task that unlocks other tasks runs first, so a build that an end-to-end test depends on gets scheduled early and the e2e runs can start sooner.

Dynamic task packing uses the CPU and memory profile learned from previous runs to pack shop:build, cart:test and admin:build onto one worker, finishing in 3.2 minutes instead of 5.1

Turning on dynamic task packing in real customer codebases (that were already heavily optimized for CI) cut their VM minutes by 35 to 50%.

Read the docs to learn how dynamic task packing works in detail.

Doing less work

Better utilization only gets you so far. To go further, CI needs to do less, and there are two ways to get there:

  • Affected - don't run tasks that can't possibly be impacted by a change.
  • Remote caching - don't run a task twice if nothing it depends on has changed.

Yesterday's Nx 24 release made task-based affected the default in Nx. It looks at tasks, the inputs they read and the outputs they write, and uses that to decide what is affected.

Task-based affected follows the chain of outputs and inputs: the ui output dist/libs/ui is an input of app and admin, and the app output dist/apps/app is an input of app-e2e

This is the same information caching uses to calculate its hash. If you change a spec file in a shared project, only the test task of that project gets invalidated.

Remote caching is the other half, and we just made it a lot better.

Ultracache

Most caching today is static. The tool either derives the cache configuration from your project graph, or you define and fine-tune the inputs and outputs of each task by hand.

That has two problems.

  • Your cache hit rate depends heavily on how the codebase is structured and how closely the configured inputs and outputs match what tasks actually read.
  • Worse, you can get false positives, which are cache hits where there shouldn't be any.

Ultracache is a fully automatic, deterministic computation cache. It learns the cache configuration from the actual runs, which gives you more cache hits and makes it correct by design.

Ultracache monitors the cart:build process on a CI worker and records which workspace files it actually reads: shell.tsx, the cart page and table, and currency.ts are read, while the checkout and search files are not

Ultracache monitors what each task reads and writes while it executes. It maps those inputs and outputs, stores them and consolidates them with what it has seen in previous runs. Your cache configuration improves with every run, and you don't have to fine-tune anything by hand.

Ultracache recorded the inputs of each e2e spec task in Nx Cloud. A change to src/features/cart reruns only cart.spec.ts, and the other five specs are cache hits

This matters even more now that AI agents write a lot of tests, especially end-to-end tests. Nx Cloud already splits a large end-to-end test target into individual task runs per spec file. Dynamic task packing spreads those tasks across CI workers with high utilization. Ultracache now caches each of them individually, so when something changes, only the spec files that depend on it run again and every other one is a cache hit.

With task-based affected, dynamic task packing and Ultracache combined, CI in our monorepo benchmark with 511 projects got 9.2x faster with 52% less compute.

Because Ultracache works from real execution data, it doesn't depend on how your code is structured. If you have one large application, a project-level cache can't help much, since any change invalidates the whole project. We ran the same setup on a single React app with about 300,000 lines of code, and CI got 11.7x faster with 78% less compute.

New workspaces start with Ultracache turned on. For an existing workspace, you can enable it in the workspace settings. Read the docs for the details.

Truly intelligent CI for AI Agents

Dynamic task packing and Ultracache both build on the same idea, which is CI driven by real execution data. Packing fits more compute into fewer VM minutes, and Ultracache makes the cache more precise and correct by design. The result is faster CI that doesn't inflate your compute bill.

Both features are available today in Nx Cloud.

Learn more