Documentation · Caching
Sticky disks
A sticky disk is a block device kept between jobs for one repository, for build tools with their own on-disk caches. Asking for one, using it, and guarding for its absence.
A job gets a fresh virtual machine every time, which is the property that makes one customer's build unable to observe another's. It is also why a build tool with its own on-disk cache starts cold on every run.
A sticky disk is a second block device attached to the job and kept between jobs for the same repository. The root filesystem is still new every time; only this disk persists.
Sticky disks are off until your organisation asks for them. An owner, an admin or a billing member asks on Settings, on the Sticky disks card, with Request sticky disks. Other members see the card but cannot ask.
A request is not switched on by itself yet: a person on our side turns it on. Until it is on, no job gets a disk, GHWARP_STICKY_DISK is empty, and nothing is charged for one. The card reads Asked for from the request until a disk has been billed, and Active after that, so Asked for does not tell you whether it has been turned on: a job that finds GHWARP_STICKY_DISK set does. The same card withdraws the request.
When a job has a sticky disk it is mounted before your steps run and the path is in the environment as GHWARP_STICKY_DISK. Point a tool's cache directory at it:
jobs:
build:
runs-on: ghwarp-8vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- name: Build
env:
GOCACHE: ${{ env.GHWARP_STICKY_DISK }}/go-build
GOMODCACHE: ${{ env.GHWARP_STICKY_DISK }}/go-mod
run: go build ./...There is no action to add and no token to configure. The disk is ext4, owned by the runner user, and yours to lay out however you like.
Guard on the variable rather than on the path. GHWARP_STICKY_DISK is set only when a disk was actually mounted, so a step that reads it gets an empty value on a job without one, and a script that hardcodes the mount point will write into the root filesystem instead and quietly lose the cache:
CACHE_DIR="${GHWARP_STICKY_DISK:-$RUNNER_TEMP}/gradle"
mkdir -p "$CACHE_DIR"A disk that cannot be prepared does not fail the job. The build runs without it and is merely slower — a cache is an optimisation, and failing a build over one would be the wrong trade.
Anything that keeps a cache on disk and is slow to rebuild. Point the disk at a tool's cache directories, never at a whole home directory or a tool's whole configuration directory:
Bazel's output base and repository cache
Gradle's
~/.gradle/cachesand Maven's~/.m2/repositoryCargo's
~/.cargo/registryand~/.cargo/git, andtarget/Go's build and module caches (
GOCACHE,GOMODCACHE)node_modulesfor a very large install
Never put a credential on a sticky disk. Later builds of the repository start from what is on it, and not all of them are as trusted as the one that wrote it. That is why the list above names ~/.cargo/registry rather than ~/.cargo, which also holds credentials.toml. The same goes for ~/.npmrc, ~/.docker/config.json, ~/.gradle/gradle.properties and any cloud CLI's configuration directory: keep them off the disk, and keep anything a step writes from a secret there too.
If one gets there anyway, or you think the disk holds something it should not, reset it.
For dependency archives that restore cheaply from a keyed tarball, the Actions cache is simpler and is included. For Docker image layers specifically, the layer cache is the right mechanism.
One disk per repository. Two repositories never share one, and neither do two organisations — it is attached to the VM as a device, and a job can reach only the one attached to it.
Every job gets its own copy. When several jobs of the same repository run at once, each one is given its own private copy of the disk as it was last saved, on whichever machine it lands. Jobs never share a live disk, and nobody waits for anybody else to finish with it. A matrix that fans out ten jobs gives all ten the warm cache.
A job's changes become what later jobs start from only if its copy is saved (see which builds keep their changes). When several builds that may save finish close together, they are saved one after another and the last one wins: each later build starts from whichever was saved last. A build whose change was still waiting to be saved when a later one of the same branch finished is dropped in favour of the later one, and the disk's history says so.
You can tell whether a disk was mounted from inside the job: GHWARP_STICKY_DISK is set only when one was, so a step that reads it gets an empty value on a job that ran without one. That is rare -- it happens only when one of our machines is short of space for the copy -- and the job then runs without the cache rather than waiting or failing.
Never treat a sticky disk as shared mutable state between concurrent jobs: each sees only the last saved state, never what a job running beside it writes.
The first build of the default branch on a new repository formats it, so treat that run as cold.
A disk is reclaimed after seven days without a job attaching it, and once it is gone it is not billed. Attaching resets the clock, so a repository that builds even weekly keeps its disk; one that stops building loses it, and the next build after that starts cold and formats a new one. Nothing is deleted while a job is holding it.
Nothing here is durable, and that is the design rather than a limitation: everything on a sticky disk must be reconstructible by the build. If losing it would break you, it belongs in an artefact store or a registry.
A sticky disk is state every later build of the repository starts from, so which builds may change it is decided on our side, not by your workflow file. In short:
A successful build of your default branch, started by a trusted trigger such as a push or a schedule, keeps what it leaves, unless what it changed holds a credential or could not be checked in time.
Every other build of a branch of the repository works on a private copy that is thrown away, or, in a protected repository, on an empty disk.
A build from a fork, a build that runs a tag, and a build whose workflow run we could not read get no disk at all.
Protecting a sticky disk has the whole of it: the exact triggers, the credentials we refuse to keep, the record of which build changed the disk, resetting it, and protected repositories.
A sticky disk is metered by what it stores, on the blocks actually used rather than on the size the disk reports. The pricing page has the rate.