Scale – From personal AI to organisational AI: why copilots plateau, and what an AI operating system changesRegister →
Back to Insights

Bun 1.4.3 at Scrydon: Our Type-Check Gate Moves to bun check

Bun 1.4.3 shipped bun check, a TypeScript type checker ported from typescript-go, on 10 October. The same day we moved the type-check gate of Scrydon's monorepo onto it. Across 104 TypeScript projects it matched tsc byte for byte in 103. A cold check of the whole repository fell from 124.9 s to 51–68 s, peak memory fell by about a third, and an entire build phase disappeared. It does not beat tsc's incremental mode on a warm re-check, and one project stays on tsc until a bug is fixed.

Xavier Geerinck

On 10 October, Bun released Bun 1.4.3. Most of the release is the usual run of runtime improvements. One line in it mattered more to us than the rest: bun check, a TypeScript type checker built into Bun.

The same day, a branch moving every type-check in Scrydon's monorepo onto it was up for review. Two months ago we wrote about Bun 1.4 cutting CPU and memory in the services we run. This post covers the other end of the pipeline: the type-checker that every change has to get past before it ships.

Cold full check
−46 to −59%
124.9 s → 50.7–67.9 s

Two runs of the whole graph; the range is the honest number.

Peak memory
−31 to −33%
13.7 → 9.2–9.5 GB

Peak resident memory of the whole process tree.

Diagnostic parity
103 / 104
projects byte-identical

The one mismatch stays on tsc, with an expiry date.

Local, Apple silicon. Every type-check task in the monorepo, forced cold, packages already built. TypeScript 7.0.2 tsc → Bun 1.4.3 bun check.

These are local measurements on the branch, not CI figures yet. We will report what the gate does in CI once it has run there. They are still enough to explain why we moved on release day.

Why bun check Mattered to Us

In our monorepo, type-checking was the job that used the most memory. Our repository has 92 TypeScript packages and apps, and 104 type-check projects. On a developer laptop, a full cold check took two minutes and peaked near 14 GB. In CI it was the reason we capped type-check concurrency at two: more than that ran a 7 GB runner out of memory.

Part of the cost was not type-checking at all. tsc reads a referenced project's emitted declaration files, not its source. So before anything could be checked, a separate phase depended on 53 package builds, emitted their declarations and cached the result between runs. Measured on its own, that phase took 67.5 s and 4.1 GB. It existed only to stop the checker from failing with "output file has not been built from source file".

bun check changes both halves. Bun describes it as a port of typescript-go, the Go rewrite of the TypeScript compiler. It reads tsconfig.json, reports the same errors as tsc and uses every core. It also follows project references from source, and it writes nothing: no JavaScript, no .d.ts, no .tsbuildinfo.

Bun's published results are strong:

  • 3x to 6.4x faster than tsc 7.0.2 on a 16-core Apple silicon Mac, with 2.2x to 4.9x less memory;
  • all 51,210 tests of TypeScript 7.0.2's conformance suite pass on every commit;
  • identical output to tsc in 1,102 of 1,103 configurations across 72 open-source projects.

We wanted to know whether the same held for ours.

What We Changed

As with Bun 1.4, we upgraded Bun as one runtime contract, not as a single version string:

  • the root package-manager pin, our shared GitHub Actions set-up and every workflow override;
  • every Dockerfile, on digest-pinned oven/bun:1.4.3 images;
  • local runtime tooling and development documentation; and
  • the Bun type definitions. @types/bun 1.4.3 was not yet published, so we pinned bun-types 1.4.3 directly.

Then the gate itself changed:

  • 91 of 92 packages now type-check with bun check.
  • The declarations phase is gone. That includes the root task, its solution-level config, the CI step that emitted declarations and the cache that held them.
  • Package builds still use tsc. bun check only checks. Our published packages still need emitted .d.ts, so only the check moved.

A test in the repository fails if any package's type-check drifts back to tsc. It also fails if a type-check drops the flags described below. The one deliberate exception has to carry a guard that expires it.

What We Measured

We ran both pipelines over the same state. Every type-check task in the repository ran with Turborepo's cache forced off, packages already built.

The old pipeline was the declarations phase followed by every check. The new one is every check, run twice. We report both runs because they differed by 17 seconds. A best-of-one figure would overstate the result.

For a single heavy project we also measured tsc warm. That is its incremental mode: .tsbuildinfo already on disk, after no change and after a one-line change.

Method
Comparison
TypeScript 7.0.2 tsc → Bun 1.4.3 bun check
Scope
Every type-check task, 104 projects
Cache
Turborepo --force; packages pre-built
Runs
Old pipeline once; new pipeline twice
Memory metric
Peak RSS of the process tree
Machine
Local Apple silicon, not CI
tsc · TypeScript 7.0.2bun check · Bun 1.4.3

Wall time, whole repository, cold

seconds
bun check, run 1
Cold
124.9 50.7−59.4%
bun check, run 2
Cold
124.9 67.9−45.6%

Peak memory, whole repository, cold

GB
bun check, run 1
Cold
13.7 9.5−30.7%
bun check, run 2
Cold
13.7 9.2−32.8%

Wall time, one heavy service

seconds
Cold
No prior state
9.90 1.57−84.1%
Warm, one-line change
tsc incremental
0.81 1.57+93.8%

Peak memory, one heavy service

GB
Cold
No prior state
3.50 0.83−76.2%
Warm, one-line change
tsc incremental
0.85 0.83−2.1%
Each measure is plotted on its own scale. The whole-repository rows compare one cold tsc run with each of the two bun check runs. The single-project rows are one of our heaviest services; its warm, unchanged tsc re-check (0.15 s, 40 MB) is in the table below rather than the plot.
RunWall timeChangePeak memoryChange
Whole repository, tsc, cold
Declarations phase + every check
124.9 s—13.7 GB—
Whole repository, bun check, two runs50.7 s / 67.9 s46–59%9.5 / 9.2 GB31–33%
Whole repository, tsc, warm
Incremental .tsbuildinfo, for reference
29.8 s—5.4 GB—
Declarations phase alone
Removed entirely
67.5 sGone4.1 GBGone
One service, cold9.9 → 1.57 s6.3x faster3.5 → 0.83 GB4.2x less
One service, warm, one-line change0.81 → 1.57 s1.9x slower0.85 → 0.83 GBFlat
One service, warm, nothing changed0.15 → 1.57 s10x slower0.04 → 0.83 GBHigher
bun check has no incremental mode, so it pays the same cost every run. The warm tsc rows are what it gives up.

The single-project result lands inside Bun's own published range: 6.3x faster and 4.2x less memory on a cold check. The whole-repository result is smaller, roughly 2x faster and a third less memory. We have not profiled where the gap goes. A whole-graph run includes Turborepo scheduling and dozens of small packages, which a single-project benchmark does not.

The biggest structural gain does not show up in a speed ratio at all. A phase that cost 67.5 s and 4.1 GB is now gone, and so is its cache. The check also lost a failure mode. Stale .tsbuildinfo files, left behind by a branch switch or a renamed checkout, used to produce hundreds of false type errors until someone deleted the cache. bun check keeps no state, so the check itself can no longer go stale.

Where tsc Still Wins

Not everywhere. tsc's incremental mode is very good at re-checking a project you just checked. On our heavy service, re-checking after a one-line change took tsc 0.81 s and bun check 1.57 s. With nothing changed, tsc answered in 0.15 s.

bun check has no incremental mode. Every run is a cold run, just a much faster cold run.

That defines the trade.

  • CI runners and fresh worktrees always start cold. There, bun check wins on every measurement we took.
  • A developer re-checking one package in a loop was better served by a warm tsc.

We made the switch because cold is what the gate actually runs. A fast, stateless check that every machine runs the same way was worth more to us than the warm-loop edge.

bun check has no language server either. Editors still run TypeScript's own, so this change does nothing for an editor that feels slow.

What We Learned Moving 92 Packages

The upgrade was mechanical. Getting the gate right took four lessons, and each one would have failed silently.

1. Pin the output format. Under an AI coding agent, bun check switches to an XML report. Our error-count ratchet read that as zero errors. Separately, it groups more than 50 identical errors into one entry, which undercounts any check that counts errors. Every type-check therefore runs with both flags:

"typecheck": "bun check -b --all --no-pretty"

A repository test fails if either flag is missing. The same audit found an older bug: tsc switches to coloured output whenever FORCE_COLOR is set, even to 0. That broke the same parser. --no-pretty overrides both.

2. A script named check shadows bun check. bun <name> runs a package script of that name before it reaches a built-in. We had 28 check scripts. In one service, check was tsc -b, so typing bun check there quietly ran the old checker. All 28 were renamed or folded into typecheck.

3. Deleting a phase deletes what it was checking. The declarations phase looked like plumbing. It turned out to be the only type-check two shared packages ever had. One contained a compile-time test written to catch a past outage. Removing the phase without noticing would have removed that test too.

The same phase hid a build-order dependency. It depended on 53 builds, so it happened to build four packages before they checked themselves. Each of the four imports its own published entry points, which resolve to its own build output. Without that ordering, one failed outright and three passed by scheduling luck.

Each now has an explicit edge, and a test asks the compiler where every self-import resolves. If an edge goes missing, or one is no longer needed, the test fails.

4. One exception, with an expiry date. In one project, bun check reports two "type instantiation is excessively deep" errors (TS2589) that tsc 7.0.2 does not. They are deterministic, and the minimal reproduction is two files.

That project stays on tsc. Its type-check also runs a guard that executes bun check on the reproduction. The guard fails once Bun stops reporting the error, so the exception cannot outlive the bug.

What This Means for Scrydon Users

Users will not see the type-checker directly. They will see how quickly a fix gets from a pull request into a release. Type-checking runs on every change, so a gate that runs cold in half the time, with a third less memory, shortens that path.

As with Bun 1.4, we will not claim more than we have measured. Until the gate has run in CI, these numbers describe our laptops.

What Comes Next

We kept CI type-check concurrency at two. That cap was set because tsc ran out of memory, and bun check now uses far less. Raising it is the obvious next step, but only once a CI run has measured it.

Bun 1.4.3 also adds bun test --check and bun build --check. Both make type errors fail a test run or a build directly, and we will evaluate whether they belong in our package pipelines. Bun also reports that servers use 67–89% less idle CPU than on 1.4.2. That is a runtime claim, so we will measure it the way we measured Bun 1.4.0: on staging, against the previous release, before saying anything about it.

For now, the result is simple. A type-checker that reports the same errors as tsc in 103 of our 104 projects, needs nothing built first and keeps no state is now our gate. The one exception is guarded and expires on its own.

Newsletter

Get the next one in your inbox

If this was worth your time, the next one will be too. We write when something changed in European AI sovereignty, or when we learned something building for it. A few times a year, no drip campaign.

  • Analysis, not announcements
  • Field notes from defence, government and finance deployments
  • First seat at the live briefings

A few times a year. No drip campaign, unsubscribe in one click. Privacy policy

Rather talk it through? Book a 30-minute briefing