Bun crossed 2 million weekly npm downloads in late 2025, sits at v1.3.x with installs 20-40x faster than npm and roughly 4x Node.js's requests-per-second in published benchmarks, and now runs in places that used to be Node-only territory: Vercel ships native Bun runtime support for Next.js, Anthropic's Claude Code CLI ships as a Bun single-file executable, and Railway's own Functions product executes user code on Bun by default.
That last one has a twist. Railway runs Bun in production — and Railway's own build system has open bug reports for failing to detect Bun in newer projects. Not because Bun got harder to spot. Because Bun changed what "spot" means, and a lot of detection code never got the memo.
Railway Functions are single-file TypeScript services that deploy straight from the Railway canvas, no git repo required: import any npm package with zero build step, call native Bun APIs like Bun.file() and Bun.serve() directly, and get instant redeploys because there's no compile stage in the way. That's Bun running production traffic today, on a platform that also happens to be one of the three vendors whose own builder — Railpack, the general-purpose git-push build system Railway ships for everything that isn't a Function — currently mis-detects newer Bun projects. Railway trusts Bun enough to build a product feature on top of it and still has an open ticket for its detector missing it. That gap is the whole story of this post in miniature.
The one-line change that broke a generation of detectors
Every buildpack-style build system — Heroku's Cloud Native Buildpacks, Railway's Railpack, Cloudflare Pages, Netlify, DigitalOcean's App Platform — runs a detect phase before it builds anything: look at the repo, decide which language/runtime buildpack applies. For years, the Bun signal was simple: does bun.lockb exist? If yes, Bun project. If no, fall through to Node.
Bun 1.2, shipped in early 2026, switched the default lockfile from that binary bun.lockb to a text-based, JSONC-formatted bun.lock — specifically so lockfiles would render in GitHub diffs instead of showing up as an opaque binary blob in every PR. Bun still supports bun.lockb for existing projects, but every new bun install writes bun.lock by default.
Detection code that only checks for bun.lockb doesn't error out on a bun.lock project. It just quietly doesn't match — and the build falls through to whatever the next detector in line is, usually Node's. The build still runs. It's slower, and on a platform whose whole value prop to a Bun-first team is "we treat your runtime as first-class," it's the wrong runtime silently substituted for the right one.
This isn't hypothetical. It's already showing up as user-filed bugs, three platforms deep:
- Railway — "Railpack does not recognise bun," with the workaround being to force it via a
RAILPACK_PACKAGES=bun@latestenvironment variable. - Cloudflare Pages — "Bun not detected as tool when using new bun.lock instead of bun.lockb."
- Netlify — an open feature request, "Support new bun.lock text lockfile for Bun," still pending as of this writing.
Same root cause, three separate platforms, three separate tickets. That's the signature of a heuristic that was correct when it was written and became wrong when the ecosystem moved — the exact failure mode a package-lock.json-only Node detector already has for any non-npm project, just recurring one runtime later.
What a detector that gets it right actually checks
DigitalOcean's App Platform buildpack is the instructive counter-example: it checks for either lockfile format, so it didn't break when the default flipped. Its detect phase, in priority order:
| Signal | What it means |
|---|---|
bun.lock present | Bun project, current default (Bun ≥1.2) |
bun.lockb present | Bun project, legacy binary lockfile (Bun <1.2, or unmigrated) |
.bun-version file | Explicit version pin, wins over auto-resolution |
.runtime.bun.txt file | Platform-specific version pin (App Platform convention) |
BUN_VERSION env var | Highest-priority override, checked before any file |
| No version signal | Falls back to latest Bun GitHub release |
That's the full signal set a build layer needs — not just "does a lockfile exist" but "which lockfile, and which version." Written as a detect-phase check, the logic that actually survives a lockfile-format migration looks like this, evaluated in order and stopping at the first match:
if env.BUN_VERSION: use env.BUN_VERSION
elif file(".bun-version"): use pinned version
elif file(".runtime.bun.txt"): use pinned version
elif file("bun.lock"): detected = true, version = latest
elif file("bun.lockb"): detected = true, version = latest
elif pkgJson.engines?.bun: detected = true, version = engines.bun
elif pkgJson.packageManager?.startsWith("bun@"):
detected = true, version = parsed from packageManager
else: fall through to Node detectorA detector keyed on a single filename — just bun.lockb, say — is one Bun minor version away from silently breaking, as three platforms just demonstrated in the same release cycle. A detector keyed on this full signal set breaks only if Bun stops writing every one of these markers simultaneously, which is a much higher bar.
"Build with Bun, run on Node" is the correct default — not a compromise
Here's the part that matters more than fixing a filename check: even after a platform detects Bun correctly, defaulting every detected project straight to a full Bun runtime is the wrong move for most of them.
DigitalOcean's App Platform handles this with a specific hybrid case: if package.json declares "packageManager": "bun@1.x.x" but a build/start script still invokes node, App Platform installs Bun to run bun install — inheriting the 20-40x faster install times — and then runs the app on Node.js (v22.x by default, overridable via engines.node). Bun becomes the package manager. Node stays the runtime.
That split matters because "faster installs" and "faster runtime" are separable claims with different risk profiles:
- Bun-as-package-manager is close to zero-risk for an existing Node project. It reads the same
package.json, resolves the same npm registry, and produces a lockfile — nothing about how the app executes changes. - Bun-as-runtime touches native modules, Node-API (N-API) compatibility, and any code that depends on Node-specific internals. Bun's Node compatibility is extensive and improving fast, but "extensive" isn't "total" — an existing production app built up over years of Node-only dependencies is exactly the population most likely to hit the remaining edge cases, and least able to afford discovering them at deploy time.
Bun 1.3 makes the install-time win concrete rather than a marketing number: the release added bun install tarball streaming that uses 17x less memory, gzip decompression 5.5x faster via zlib-ng, and AbortSignal.timeout running 40x faster — all wins a project gets the moment its dependencies are installed with Bun, regardless of what runs the app afterward. None of that requires touching the runtime a single line of application code executes on. That's the entire case for treating "install with Bun" and "run with Bun" as two separate feature flags instead of one all-or-nothing switch.
Vercel's approach is the mirror image, and the contrast is useful: native Bun runtime support for Next.js exists, but it's explicit opt-in — set "bunVersion": "1.x" in vercel.json, or run scripts with bun --bun — not something Vercel autodetects and silently switches on. Two platforms, two philosophies, same underlying caution: don't flip a project's runtime without the developer asking for it. Autodetect the package manager; ask before you touch the runtime.
What this means for a git-push PaaS's build layer
For a platform like Bex — push a git repo, get a running HTTPS service on machines you own — the buildpack/Dockerfile-base-image logic needs three things, in this order:
- Detect both lockfile formats, not one. Check
bun.lockORbun.lockbOR.bun-versionORpackageManagerinpackage.jsonORengines.bun. Any one of these is sufficient; missing all of them is the only case that should fall through to a Node-only build path. - Default new deploys to the hybrid: install with Bun, run with Node, unless the project's own scripts or
enginesfield prove a full-Bun runtime is intended (e.g., start script already invokesbun run, orengines.nodeis absent whileengines.bunis present). This mirrors DigitalOcean's App Platform behavior and gives every existing Node project the install-speed win with none of the runtime-compatibility risk. - Never let a version bump silently degrade the build. The Railway/Cloudflare/Netlify tickets above are a warning specific to Bun's lockfile transition, but the pattern generalizes: any detect phase keyed on a single file's presence needs a test that fails loudly when that ecosystem's tooling adds a second valid signal — not a silent fallthrough to a slower, wrong-runtime build that nobody notices until someone benchmarks it.
The pragmatic recommendation isn't "rewrite your app in Bun." It's "let Bun install your dependencies, and only switch what actually executes your code when you've asked for it by name."
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



