Setup time, caching and billing
The job environment covers what a job can run with. This page covers what it costs: how the environment reaches the job’s virtual machine, what is reused between runs, how to keep setup short, and which part of a job’s time you are billed for. It separates what happens today from what is planned; nothing marked planned is in effect yet.
What happens before your first step
Section titled “What happens before your first step”A hosted job goes through these phases in order:
| Phase | What happens | Billed |
|---|---|---|
| Queue | The job waits for a runner with room for its size: |
no |
| Host preparation | The runner node forks the repository’s cache disk and prepares the read-only disk of the job’s actions | no |
| VM boot | The job’s VM starts and its guest connects to the runner | no |
| Services | services: containers start under the VM’s dockerd |
yes |
| Steps | Your steps run, with the packages: or flake: environment set up before the first step that needs it |
yes |
Environment setup sits among the steps rather than before them: a job whose
environment comes from the workspace (flake: .#ci) sets it up after
actions/checkout, just before its first run: step. The log shows it as its
own ==> setup: phase. The step list does not show it as a step yet, so its
time is counted inside the job’s step timings.
How environments are delivered and cached
Section titled “How environments are delivered and cached”What is cached today
Section titled “What is cached today”| Part of the environment | Where it comes from in each job | Reused between jobs |
|---|---|---|
| The base environment and its toolchains | The VM’s root image | yes: nothing is downloaded |
setup-* tool versions not in the image |
A checksum-verified mirror neosource keeps | yes, through the mirror |
Actions (uses:) |
A SHA-pinned mirror, mounted read-only into the VM | yes, through the mirror |
packages: and flake: (Nix) |
cache.nixos.org, downloaded and unpacked into the VM’s own empty Nix store; anything cache.nixos.org has not built is built in the job | no (preloading is rolling out) |
| Build output and download caches | The repository’s cache disk (below) | yes, from the last trusted run on a protected branch |
actions/cache entries |
The platform’s cache service | yes, within the repository |
Nix environments are not reused between jobs today. Every hosted job
starts with a Nix store that holds only Nix itself and the source of the
platform’s pinned nixpkgs revisions, so evaluating at the pin needs no
download. Everything the environment contains is then downloaded from
cache.nixos.org, in every job, even when the previous job of the same
repository used the same flake.lock. Measured on neosource’s own CI:
- a small set of bare
packages:is ready in about 2–4 s (about 620 MiB in about 3.5 s for a deploy job); - multi-GiB flake dev shells take 8–28 s, of which evaluating the flake is 3–5 s;
- every derivation cache.nixos.org does not have is built again in every job.
The cache disk. Each repository’s jobs (every matrix leg on its own) get a
copy-on-write disk on the runner node’s NVMe that persists between runs:
Cargo’s build output ($CARGO_TARGET_DIR), ~/.cache, Cargo’s registry and
git caches, Go’s module cache, the npm, bun, pnpm and Yarn download caches,
and Docker’s images and build cache. Not on it: your checkout,
node_modules, ~/.cargo/bin and the Nix store. Every job starts from a
private fork of the last committed disk. A job’s copy becomes the next
generation only when the run is trusted, was triggered by a push, a schedule
or workflow_dispatch, and builds a branch that has a branch protection
rule. Pull request runs (from a fork or not) and merge queue runs read the
cache and never write it back. The disk lives on one runner node; the
scheduler prefers a node that already holds it, and a job placed elsewhere
starts cold.
actions/cache works against the platform’s cache service, scoped to the
repository and evicted least recently used first. Runs from forks can read
entries and cannot write them. The repository’s Actions → Caches page lists
what is stored.
On your own machine (neo runner start, neo push --runner, neo run)
the job uses the machine’s own Nix store, which persists between jobs, so on a
warm machine the setup line reads already in the local store. A
self-hosted VM runner behaves like the
hosted pool: each job’s VM starts with an empty store.
Rolling out: environments recorded from your default branch
Section titled “Rolling out: environments recorded from your default branch”This is being rolled out and is not in effect yet: until it is, no job gets a preload and setup works as described above. Once it is:
- A trusted run records its environment. When a run on a protected branch
(a push or
workflow_dispatch, not a pull request) sets up aflake:environment from your repository, or a set of barepackages:, it records the environment’s store paths. A later job whose environment matches gets the paths cache.nixos.org has signed preloaded from a read-only disk on the runner node, pull requests included. A flake environment matches whenflake.nix,flake.lockand the filesflake.nixnames with./paths are byte-identical to the recorded commit’s; barepackages:match when they name the same packages at the same platform pin, in any order. A flake job’s log then says, before setup,environment: N store paths preloaded from the runner's cache; for barepackages:the preload shows only as fewer downloads in the setup line. - The first matching job on a node prepares the disk in the background and sets up as before; it never waits for the disk, and neither does any later job. Only jobs on that node after the disk is ready get the preload.
- Not preloaded: anything cache.nixos.org has not signed (your local
builds, fixed-output downloads, flake inputs), environments a pull request
changes until the change reaches the default branch, versioned or mixed
packages:, flakes from outside the repository, and self-hosted runners. A node keeps every repository’s disks in one packages budget, evicting the least recently used first; one environment may take at most a fifth of it. - Your environment is still yours. The job still evaluates its own environment and downloads or builds whatever the disk lacks, so a record that is out of date makes setup slower, never different.
Planned
Section titled “Planned”These are planned and not in effect:
- What your repository builds or fetches itself. The preload above covers only content signed by cache.nixos.org; it is stored once per runner node and shared by every repository. Local builds and fixed-output downloads (flake inputs included) will be kept private to your repository, under a per-repository cap with least recently used eviction, and written only by trusted runs. A heavy flake therefore costs its owner the first run’s time, not other tenants.
Bare packages: stay the cheapest choice under the plan: what they need is
almost entirely cache.nixos.org content, which is the shared tier.
Keeping setup fast
Section titled “Keeping setup fast”- Use what is already there. The base environment’s toolchains cost
nothing. Prefer them, then bare
packages:, then versionedpackages:; reach forflake:when you need its pins or its custom tools. - Keep dev shells lean, one per kind of job. A lint job does not need the
browser your end-to-end tests use. Every path in a shell is downloaded in
every job that uses it today, so split a large shell into
devShells.lint,devShells.testand so on, and point each job at the one it needs. - Avoid derivations cache.nixos.org has not built. An overlay that changes
a compiler flag, an
overrideAttrs, a toolchain assembled from components, your ownmkDerivation: each is built in every job today, and the setup line names the slowest ones. Prefer the package as nixpkgs ships it. - Pin nixpkgs to a revision cache.nixos.org has built. Follow a channel
(
nixos-unstable,nixos-25.05) rather than nixpkgsmaster: a channel advances only after Hydra has built it, while amastercommit can leave many packages to build in your job. Pinning your flake’s nixpkgs to the platform’s revision (neo versionprints it) also skips the nixpkgs source download, because the VM image already carries that source. - Mind the number of jobs. Each job sets up its own environment and is rounded up to a whole minute separately (see What you’re billed for), so splitting work into many short jobs multiplies both.
Reading the setup line
Section titled “Reading the setup line”Every job that uses packages: or flake: logs one header and one summary:
==> setup: nix flake shell .#cinix environment ready: substituted 133 paths (3.4 GiB unpacked): cache.nixos.org 133 (3.4 GiB); built 2 derivations in 14.2s (inputs 0.8s · eval 4.1s · substitute 8.9s · build 1.0s: nix-shell-env 0.6s, bun-1.4.4 0.4s)| Field | Meaning |
|---|---|
substituted 133 paths |
Store paths downloaded instead of built |
(3.4 GiB unpacked) |
Their size on disk after unpacking, not the smaller compressed download |
cache.nixos.org 133 (3.4 GiB) |
The same, per source. A source named node store is the runner machine’s own store |
built 2 derivations |
Derivations built in the job, because no source had them |
in 14.2s |
Wall-clock time of the whole setup |
inputs 0.8s |
Fetching flake inputs (such as a nixpkgs source) while evaluating |
eval 4.1s |
Time no download or build covers: evaluating the flake or packages, plus Nix’s startup and cache lookups |
substitute 8.9s |
Downloading and unpacking the environment’s paths |
build 1.0s: nix-shell-env 0.6s, … |
Wall-clock time during which a build ran, with the slowest builds named (five at most, then +N more). build 3.0s wall, 5.6s summed means builds ran in parallel |
Once preloading
is in effect, paths preloaded from the runner node’s disk (the preloaded from the runner's cache line before setup) are already in the store when Nix starts, so they
appear in no count here: a preloaded job shows fewer paths and bytes from
cache.nixos.org, not a new source.
Downloads and builds can overlap, so the phases add up to roughly, not
exactly, the total. A phase under 0.05 s is left out. already in the local store means nothing had to be downloaded or built. nix environment ready in 6.3s with no detail means Nix reported nothing to break the time down by.
When setup is slow
Section titled “When setup is slow”Read the setup line and start with the largest phase:
buildis large. The named derivations are built in every job. Use the package as nixpkgs ships it, or move the tool out of the shell the job uses.substituteis large. The environment is big. Split the shell per job, or replace the flake with barepackages:for jobs that need only a few tools.inputsis large. Flake inputs are being fetched. Pin nixpkgs to the platform’s revision, and drop inputs the shell does not use.evalis large. The flake takes long to evaluate. Importing nixpkgs once rather than several times, and avoiding import-from-derivation, both help.
What you’re billed for
Section titled “What you’re billed for”Hosted CI minutes count from the moment the job’s VM is up and its guest has
connected, until the job finishes, rounded up to the next whole minute per
job. That covers everything that runs inside your VM: starting services:,
checkout, environment setup, your steps and any post-steps. It does not cover
time spent waiting in the queue, the runner preparing the job’s disks, or the
VM booting. A job that runs for 61 seconds counts as 2 minutes.
Jobs on your own self-hosted runners are not billed.
Where to see usage:
- Your workspace’s usage page (
/<owner>/billing, Usage in its settings) shows the month’s minutes against the plan’s allowance. - A repository’s Actions → Usage page (
/<owner>/<repo>/actions/usage) breaks that down for one repository. - The same figures are in the API:
GET /api/owners/{handle}/ci/usageandGET /api/repos/{owner}/{repo}/actions/usage.
A per-job billed figure and a setup timeline in the job view are planned.
A run created while the workspace has used up its monthly allowance is held
as quota_blocked and does not start; runs created after the next month
begins, or after a plan upgrade, run as usual.