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

> oven-sh/setup-bun caches only the Bun binary, and only for exact versions. Learn how it picks a version, how to cache bun install, and its errors.

Source: https://latchkey.dev/learn/actions/oven-sh-setup-bun  
Updated: 2026-11-12

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.

## 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.

| Input | Default | What it does |
| --- | --- | --- |
| `bun-version` | empty | Exact version, a range such as `1.4.x`, `latest`, `canary` or a commit |
| `bun-version-file` | empty | `package.json`, `.bun-version`, `.tool-versions` or `.bumrc` |
| `bun-download-url` | empty | Zip to download instead; skips version resolution, the AVX2 check and the cache |
| `registries` | empty | One registry per line, optionally scoped, with a token after a pipe |
| `registry-url` | empty | Deprecated: use `registries` |
| `scope` | empty | Deprecated: use `registries` |
| `no-cache` | `false` | Do not cache the executable |
| `token` | `github.token` on github.com | Authenticates the tag lookup used for ranges and `latest` |

> From `action.yml` and `src/index.ts` at v2.2.0 (commit 0c5077e), read 2026-11-12.

## 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.

| Message | What 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 frozen` | `bun ci` found `package.json` and `bun.lock` out of step. Run `bun install` locally and commit the lockfile |

> Action messages from `src/download-url.ts`, `src/utils.ts`, `src/bunfig.ts` and `src/action.ts` at v2.2.0; the Bun message from `src/install/PackageManager/install_with_manager.rs` in oven-sh/bun. Read 2026-11-12.

## FAQ

### 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

- [oven-sh/setup-bun README and action.yml at v2.2.0 (read 2026-11-12)](https://github.com/oven-sh/setup-bun/tree/v2.2.0)
- [oven-sh/setup-bun src/action.ts: executable cache](https://github.com/oven-sh/setup-bun/blob/v2.2.0/src/action.ts)
- [Bun docs: global cache](https://bun.com/docs/pm/global-cache)
- [Bun docs: bun install and --frozen-lockfile](https://bun.com/docs/pm/cli/install)
- [oven-sh/setup-bun#14: where is the dependency cache](https://github.com/oven-sh/setup-bun/issues/14)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
