Bun

JavaScript runtime, bundler, test runner, and package manager.

Goals

Replace the Node/tsc/jest/npm stack with a single tool that runs TypeScript directly, runs tests, and compiles a self-contained binary for deployment as a scheduled task — without a build pipeline.

Effectiveness

Excellent. Runs TypeScript without configuration, compiles to a standalone binary that requires no runtime at the target machine, and provides a Jest-compatible test runner. Three tools replaced by one, with no compromises on the features we actually use.

What made it effective

Bonus utility

bunx runs CLI tools from npm without installation — useful for one-off wrangler invocations during deployment without adding it as a dependency.

Friction / pain points / surprises

spawnSync comes from Node's child_process, not Bun's native API. Bun exposes Node compatibility APIs — import { spawnSync } from "child_process" works — but it's not obvious whether you should prefer Bun's Bun.spawnSync or Node's. We use the Node import; it works fine but the duality is a minor cognitive overhead.

4MB maxBuffer limit on spawnSync. Large axe outputs could approach this. Hasn't been hit, but it's an invisible ceiling that will produce a cryptic error when it's hit.

Default fetch socket timeout (~30s) aborts long Ollama calls. Bun's fetch has an undocumented default socket timeout of roughly 30 seconds. The qwen3.5:27b model cold-start takes ~55 seconds to respond even to "hi". The result is an AbortError that looks like a network failure, not a timeout. Fix: pass signal: AbortSignal.timeout(300_000) explicitly on any long-running inference call. This was responsible for the podcast warmup failing on every cold-start nightly cron run.

bunx is the container-safe replacement for npx. In a Docker container built without npm, npx is not available. bunx is the drop-in replacement and ships with Bun. Any deployment or tooling script that uses npx wrangler, npx marss, etc. must use bunx instead. This caused three separate pipeline launch failures — the first was caught in deploy.ts, the second in feed.ts, and there were no more after a global grep. The lesson: on first occurrence of an environment-specific failure, grep for every instance of the pattern before closing the loop.