Skip to content
Latchkey LogoLatchkey home

oven-sh/setup-bun caches Bun, not your packages

oven-sh/setup-bun downloads a Bun release from GitHub, puts bun and bunx on PATH, and caches only the Bun executable, and only when you pin an exact version. Your dependencies are not cached at all: ~/.bun/install/cache needs its own cache step, keyed on bun.lock.

A Bun job looks fast on the first run and stays exactly that fast, because nothing it downloads survives to the next one. The action's own cache holds a single binary, it is switched off for the default latest, and bun install fetches every package again on every run unless you add a step for it.

The fix is one extra step keyed on bun.lock, shown below. The version input matters too: pin an exact version and the action can cache its own binary and skip the release lookup, which is the call that fails when GitHub rate limits it.

Version lookup order in setup-bun, the executable cache it keeps, and the install cache it leaves to you
Two caches with two owners. Read from oven-sh/setup-bun v2.2.0 source and Bun's global cache docs on 2026-11-12.

Inputs, and a cache-hit about the binary

Outputs are bun-version, bun-revision, bun-path, bun-download-url and cache-hit. cache-hit describes the executable, never your packages.

InputDefaultWhat it does
bun-versionemptyExact version, a range such as 1.4.x, latest, canary or a commit
bun-version-fileemptypackage.json, .bun-version, .tool-versions or .bumrc
bun-download-urlemptyZip to download instead; skips version resolution, the AVX2 check and the cache
registriesemptyOne registry per line, optionally scoped, with a token after a pipe
registry-urlemptyDeprecated: use registries
scopeemptyDeprecated: use registries
no-cachefalseDo not cache the executable
tokengithub.token on github.comAuthenticates the tag lookup used for ranges and latest

How it picks a version

In order: the bun-version input; the file named in bun-version-file; then package.json, silently, taking packageManager when it starts with bun@ and engines.bun otherwise; then latest. Version 2.1.0 added the engines.bun read.

An exact version such as 1.4.4 goes straight to the release URL. Anything else, including the default latest, lists every tag through api.github.com/repos/oven-sh/bun/git/refs/tags, which is why the token input exists. On Linux x64 the action reads /proc/cpuinfo and downloads the -baseline build when the CPU lacks AVX2.

.github/workflows/ci.yml
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
    with:
      bun-version: 1.4.4   # exact: no API call, and the binary is cached

What it caches, and what it does not

The executable cache stores one file, ~/.bun/bin/bun, under a key of bun- plus a SHA-1 of the download URL. It is off when no version is set, when the version is latest, canary or action, when you pass bun-download-url, and when no-cache is true. It saves in the post step only after a successful job, and a restored binary whose version does not match the URL is discarded with a warning and downloaded again.

Dependencies are a different directory. Bun stores every downloaded package in ~/.bun/install/cache, or wherever BUN_INSTALL_CACHE_DIR points, and links them into node_modules with hardlinks on Linux. The action never touches it. A maintainer explained why in the issue asking where the cache was: "We haven't implemented dependency caching yet, because early testing showed bun install was faster than using Github's caching mechanism." Whether that holds for your dependency tree is a measurement, not a default.

Caching bun install yourself

Cache the global cache directory, key it on bun.lock, and install with bun ci. Bun does not turn on --frozen-lockfile in CI by itself; its docs say to pass the flag or use bun ci, which fails when package.json and bun.lock disagree instead of rewriting the lockfile.

A prefix restore-keys is safe here because Bun takes what it can from the cache and downloads the rest. Projects still on the binary bun.lockb should hash that file instead; Bun 1.2 made the text bun.lock the default.

.github/workflows/ci.yml
  - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
    with:
      bun-version-file: .bun-version
  - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
    with:
      path: ~/.bun/install/cache
      key: bun-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('bun.lock') }}
      restore-keys: bun-${{ runner.os }}-${{ runner.arch }}-
  - run: bun ci
  - run: bun test

Platforms, and a Bun that is already there

Before downloading, the action looks for ~/.bun/bin/bun. If it exists and its version equals the pinned one, it is used as is and cache-hit reads true, with no network request. That check, added in v2.1.0, never matches latest, canary or an unset version, so on a persistent self-hosted runner a pinned version is what stops the download on every job.

Windows on arm64 gets a native build only from Bun 1.3.10. For anything older the action logs a warning that it is using the x64 baseline build under Microsoft's emulation layer and carries on, which works but runs slower. If your matrix includes windows-11-arm, pin 1.3.10 or later.

Private registries

The registries input writes a bunfig.toml into the working directory, merging into one that already exists, so the file in your checkout changes during the job. Put tokens in environment variables on the install step rather than in the input, because the input text is written to that file as given.

.github/workflows/ci.yml
  - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
    with:
      bun-version: 1.4.4
      registries: |
        https://registry.npmjs.org/
        @myorg:https://npm.pkg.github.com/|$GITHUB_TOKEN
  - run: bun ci
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The messages you will meet

The first four come from the action, the last from Bun itself. Angle brackets mark the parts that are filled in.

MessageWhat it means
No Bun release found matching version '<version>'The range matched no tag. Check the spelling, or pin an exact version
Failed to fetch url <url>. (status code: <code>, ...)The tag lookup failed, for example on a rate limit. Pin an exact version to skip the lookup
You can't have more than one global registry.Two unscoped lines in registries
Cached Bun version <revision> does not match expected version <version>. Re-downloading.A warning, not a failure: the cached binary was stale
error: lockfile had changes, but lockfile is frozenbun ci found package.json and bun.lock out of step. Run bun install locally and commit the lockfile

Key takeaways

  • Pin an exact bun-version: it skips the GitHub API lookup and is the only way the binary gets cached.
  • setup-bun never caches ~/.bun/install/cache; add a cache step keyed on bun.lock.
  • Install with bun ci or --frozen-lockfile, because Bun does not enable it in CI by itself.
  • registries rewrites bunfig.toml in your working directory during the job.

Frequently asked questions

Does setup-bun cache dependencies?
No. It caches the Bun executable when the version is pinned, and nothing else. Packages live in ~/.bun/install/cache, and keeping them between runs takes a cache step of your own with a key built from bun.lock.
Why is the setup-bun cache-hit output always false?
Because the executable cache is switched off unless you pin a version. With no version, latest or canary, the action downloads every time and cache-hit stays false. Set an exact bun-version or a .bun-version file and the second run restores the binary.
Should bunx be available after this action?
Yes. The action creates bunx as a symlink to bun in ~/.bun/bin and adds that directory to PATH, so both commands work in every later step of the job.
How do I fix a 403 or 429 when setup-bun installs Bun?
Pin an exact version first. A range or latest makes the action list tags through the GitHub API, which is where rate limits bite, and the README recommends the token input for that lookup. An exact version downloads the release zip directly with no API call.

References

setup-bun caches the binary. Latchkey cache-action keeps your packages. Start free → 30-day trial · No credit card