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.

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 |
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.
- 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 cachedWhat 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.
- 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 testPlatforms, 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.
- 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 |
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 onbun.lock. - Install with
bun cior--frozen-lockfile, because Bun does not enable it in CI by itself. registriesrewritesbunfig.tomlin your working directory during the job.
Frequently asked questions
Does setup-bun cache dependencies?
~/.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?
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?
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?
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.