CI and build platforms

Mount the cache. Skip the tarball.

Your remote build cache handles task outputs. Put package caches, toolchains, and a repository mirror on one Archil disk: jobs mount it read-only in 50 ms, and one refresh job on main writes it.

Each job downloads and extracts the whole archive, and uploads a new one when the key changes.

Mounts on your runners

  • Linux
  • Kubernetes
  • Docker
  • Amazon Web Services
  • Google Cloud

VMs, FUSE containers, and Kubernetes pods via the CSI driver; an IAM role on AWS, a token anywhere. Mounting on Linux.

Everything your build cache doesn't hold.

A job that needs 200 MB of a 20 GB cache archive still downloads and unpacks all 20 GB.

Archives restored whole

actions/cache downloads and extracts the whole archive before the first step.

Per-host NVMe copies

A job lands warm on a host with a copy, and cold anywhere else.

Shared file systems sized for everything

FSx for Lustre starts at 1.2 TiB, and EFS bills every byte you keep.

Golden images restore cold

A restored volume pulls each block from S3 on its first read.

Stop tarring up your cache.

The job's first step becomes a mount.

Warm on a runner that just booted

Jobs that open the same toolchain at once share one S3 fetch; after that, any runner in the region reads it from Archil's SSD cache.

No save and restore step

Each job mounts the disk in 50 ms at the median.

Read what the job opens

A job reads only what it opens, and archil prefetch warms a directory it knows it needs.

Your remote cache holds outputs. The disk holds the rest.

Keep task outputs in your remote cache, and put dependencies, layers, toolchains, and the repository mirror on the disk. Most package managers can read a cache they can't write.

shared-caches.sh
Every job, read-only
# Every job: the shared cache, read-only
sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount \
dsk-XXXXXXXXXXXX /mnt/cache \
--region aws-us-east-1 --read-only
 
# Gradle: a read-only dependency cache; misses go
# to the build's own. Outputs: your remote cache
export GRADLE_RO_DEP_CACHE=/mnt/cache/gradle-ro
./gradlew build --build-cache
 
# Go: modules from the disk, then the network
GOMODS=file:///mnt/cache/gomod/cache/download
export GOPROXY="$GOMODS,https://proxy.golang.org"
 
# BuildKit: layers from the disk. (The GitHub Actions
# backend is rate limited and skips cache mounts.)
docker buildx build . \
--cache-from type=local,src=/mnt/cache/buildx

Outputs in one place, inputs in another

Bazel, Turborepo, Nx, and Gradle key outputs by their inputs and expect dependencies on the runner.

Main writes the cache. Everything else reads it.

Only main's refresh job writes the cache, so a pull request can't change what your release builds from. Every other job mounts it read-only.

Sharing disks
refresh.sh
On main, the only writer
# Your orchestrator runs this when the lockfile
# changes on main. It's the only job that writes.
sudo archil mount dsk-XXXXXXXXXXXX /mnt/cache \
--region aws-us-east-1 --conditional
 
# Fetch into the mirror; never let git collect it
git -C /mnt/cache/repo.git -c gc.auto=0 \
-c maintenance.auto=false fetch origin
 
# Add main's packages to the shared npm cache
src="$(mktemp -d)/src"
git clone -q --reference /mnt/cache/repo.git "$REPO_URL" "$src"
cd "$src" && npm ci --ignore-scripts --cache /mnt/cache/npm
 
# Make the writes durable, then record this cache
sudo archil sync /mnt/cache
archil checkpoints create /mnt/cache "cache-$(date +%Y%m%d%H%M)"
sudo archil unmount /mnt/cache
job.sh
Every other job, read-only
# Every other job, on any runner
export ARCHIL_MOUNT_TOKEN="$JOB_TOKEN" # read-only token
sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount \
dsk-XXXXXXXXXXXX /mnt/cache \
--region aws-us-east-1 --read-only
 
# Clone onto the runner, borrowing objects from the mirror
git clone --reference /mnt/cache/repo.git "$REPO_URL" src
cd src && git checkout --force "$SHA"
 
# Install from the shared cache; if this PR changed
# the lockfile, install from the registry as today
npm ci --offline --cache /mnt/cache/npm || npm ci
 
npx turbo run build test # outputs: your remote cache
sudo archil unmount /mnt/cache

A checkpoint per refresh, a branch to roll back

Each refresh ends with an immutable checkpoint in 13 ms; if one breaks builds, mount a branch of the last good one.

Checkpoints and branches (managed disks, AWS) can't be deleted yet; keep one per refresh and per rollback. Branches and checkpoints.

Put your cache volumes on Archil.

Back each customer's cache with its own disk, not a copy per host: stored once, read from any host.

  • A disk per customer is one API call: 25,000 per region on Team, unlimited on Enterprise.
  • Mount tokens are scoped to one disk; Archil stores only a hash.
  • BYOC runs Archil, managed by Archil, in your cloud, your customers', or on bare metal; disk data never leaves.
  • Data touched in the last hour bills at $0.20 per GiB-month; the rest of a managed disk, about $0.027.
dispatch.ts
TypeScript SDK
// In your orchestrator, before each job. The API
// key stays here; the runner gets only a token.
import { Archil } from "disk"
 
const archil = new Archil({
apiKey: process.env.ARCHIL_API_KEY,
region: "aws-us-east-1",
})
const cache = await archil.getDisk(customer.cacheDiskId)
 
// One mount, gone in 2 hours: outlast your longest
// job, or the runner's reads and writes fail.
// Only the refresh job on main writes; forks read.
const { token } = await cache.addUser({
type: "token",
nickname: `job-${job.id}`,
ttl: "2h",
oneUse: true,
readOnly: job.kind !== "refresh",
})
 
// The runner mounts with ARCHIL_MOUNT_TOKEN=token.
// Terraform's archil_disk_user takes the same options.

Time before the first step.

Your cache restore time and job count, against a 50 ms mount.

Your pipeline

20 s
5 s5 min
10
1100
100
102,500

Save and restore

5.6 hof runner time a day before first steps

Each job waits
20 s
Jobs a day
1,000

One mounted cache

50 sof runner time a day before first steps

Each job mounts, median
50 ms
Jobs a day
1,000

A model, not a benchmark: it counts only the wait before the first step. Saving a cache key and refreshing the disk happen only when the lockfile changes, so neither counts.

Five ways to cache a build.

If your jobs always land on a warm NVMe cache, keep it. Archil holds what a remote build cache doesn't.

Save and restore (actions/cache)

Before the first step
Download, extract everything
Reads, once warm
Local disk
Many jobs at once
A copy each
Retention
7 days unused; 10 GB per repo
Rolling back a bad cache
Save a new key

Per-host cache volumes

Before the first step
Attach a nearby copy
Reads, once warm
Local NVMe: fastest
Many jobs at once
Private copies; last commit wins
Retention
7 to 14 days unused
Rolling back a bad cache
The vendor's tools

Remote build cache (Bazel, Turborepo, Nx)

Before the first step
Nothing; outputs fetched on hits
Reads, once warm
Local disk
Many jobs at once
Safe: keyed by inputs
Retention
Your server's policy
Rolling back a bad cache
Not needed

Shared file system (EFS, FSx for Lustre)

Before the first step
Mount
Reads, once warm
NFS or Lustre
Many jobs at once
One live tree
Retention
Until deleted
Rolling back a bad cache
Your backups

Archil

Before the first step
Mount, 50 ms median
Reads, once warm
Memory, then shared SSD
Many jobs at once
Jobs read; refresh job writes
Retention
Until deleted; idle bills as archive
Rolling back a bad cache
Branch the last good checkpoint