I pointed bun fmt --check at Prettier's own source tree expecting it to take a few seconds. It finished before I'd read the command back to myself.

Bun 1.4.4 landed on 22 October with bun fmt, a formatter built into the runtime that claims to be Prettier-compatible and to replace a handful of common Prettier plugins outright. The obvious way to test a "Prettier-compatible, much faster" claim is to point it at a real Prettier project and see whether it agrees with Prettier's own idea of correctly formatted code, and how long that agreement takes to establish.

Prettier's own repository is a good subject for this, because Prettier enforces its own style on itself: yarn lint:prettier runs prettier . --check --cache in CI, so if I check out the repository fresh, every file prettier itself tracks should already pass a Prettier check. Bun's own release post benchmarked bun fmt against exactly this repository and claimed a 2,616-file source tree took 59.7ms, against 17.1 seconds for Prettier, calling it roughly 286 times faster.

I cloned prettier/prettier at a shallow depth, pointed both tools at it with their working directory set correctly, and let them disagree with Bun's marketing copy if they wanted to.

The headline number

With --cwd pointed at the checkout, bun fmt --list-files counted 2,623 matched files, which is close enough to Bun's own 2,616 that I'm confident this is the same test they ran.

Tool Run Wall time Peak RSS
bun fmt --check 1 0.10s 48.3 MB
bun fmt --check 2 0.13s 48.5 MB
bun fmt --check 3 0.15s 48.7 MB
prettier --check 1 36.77s 519.6 MB

I only ran Prettier once. A 37-second check, repeated enough times to build a proper spread, would have eaten the time I needed for everything else, and the sandbox I was working in started refusing repeated invocations of a formatter against a cloned repository partway through this run (it treats repeatedly executing a build tool against code it didn't write as something to be cautious about, which is a reasonable thing for it to be cautious about). One run is thinner evidence than three, so take the Prettier number as a single data point rather than a settled average. It's also close enough to Bun's own claim that I don't think a second or third run would move it much: 36.77 / 0.13 is 283 times, against Bun's claimed 286.

Both tools agreed the repository needed no changes. Zero files failed either check.

What I got wrong on the way

My first attempt at this comparison crashed both tools, not just Bun's.

I ran bun fmt --check /path/to/prettier from a different working directory, passing the repository as a plain argument instead of setting --cwd. It crashed immediately:

error: Cannot load the configuration file .../tests/integration/cli/config-external-config-syntax-error/package.json:
1 | "use strict";
2 |
3 | throw new SyntaxError("THIS ERROR MESSAGE MUST COME FROM THIS syntax-error.js FILE");
              ^
SyntaxError: THIS ERROR MESSAGE MUST COME FROM THIS syntax-error.js FILE
Enter fullscreen mode Exit fullscreen mode

That file exists on purpose. It's one of Prettier's own CLI tests, checking that Prettier reports a broken external config correctly, and it's listed in .prettierignore under /tests/integration/cli/. My first instinct was that I'd found a real bug in bun fmt's config resolution: it was walking into a directory the ignore file explicitly excludes.

Then I ran the exact same invocation through real Prettier, from the same wrong working directory, and it crashed too, with the same underlying error, after spending 100 seconds getting there instead of a fraction of a second. Neither tool had a bug. I had a bug: run from outside the target directory, with no --ignore-path or --cwd pointing at it, neither tool reliably finds .prettierignore next to the files it's checking, and both end up trying to load a config they were supposed to skip. Once I passed bun fmt --check --cwd <repo> and prettier <repo> --check --ignore-path <repo>/.prettierignore, both tools skipped the test fixtures correctly and the numbers above are from that corrected setup.

It isn't as Prettier-compatible as it sounds

Bun's documentation says bun fmt includes native replacements for a few well-known Prettier plugins — Tailwind class sorting, package.json key sorting, Svelte support — specifically so you don't need to install them. I wanted to know what happens with a plugin that isn't on that short list.

I wrote a .prettierrc.json referencing prettier-plugin-organize-imports, in a project where that package was never installed, alongside a file with two unused imports:

import { z } from "zod";
import   React   from "react";
const x = {a:1,   b:2};
export function hello(name:string){return `hi ${name}`}
Enter fullscreen mode Exit fullscreen mode

Real Prettier refused outright:

[error] Cannot find package 'prettier-plugin-organize-imports' imported from .../noop.js
Enter fullscreen mode Exit fullscreen mode

bun fmt did not refuse. It formatted the file, exit code 0, in 2.86ms, and both unused imports were gone from the output as if the plugin had actually run:

const x = { a: 1, b: 2 };
export function hello(name: string) {
  return `hi ${name}`;
}
Enter fullscreen mode Exit fullscreen mode

bun fmt --doctor explains why: it lists prettier-plugin-organize-imports under "Built-in plugins". Bun recognises that specific package name and substitutes its own native reimplementation, without ever checking whether the real package is installed, or which version of it you'd have gotten.

That's a deliberate design choice and a sensible one for the handful of plugins Bun has actually reimplemented. But it means "Prettier-compatible" has a quiet asterisk: your config can name a plugin Bun has never loaded and never will, and nothing will tell you that the behaviour you're seeing is Bun's guess at what that plugin does rather than the plugin itself.

I checked where the line actually sits by using a plugin name with no native equivalent, prettier-plugin-totally-fake-xyz:

error: 1 file was not formatted because the Prettier config uses plugins that bun fmt does not include: prettier-plugin-totally-fake-xyz
  Use --allow-unsupported to format it without the plugins.
Enter fullscreen mode Exit fullscreen mode

Exit code 2, nothing written. That's an honest refusal, and a better failure mode than silently guessing. The documented workaround, --allow-unsupported, did exactly what it says:

warn: prettier-plugin-totally-fake-xyz in .../.prettierrc.json has no effect because bun fmt does not include that plugin
Formatted 1 file, 0 unchanged [1.98ms]
Enter fullscreen mode Exit fullscreen mode

It formatted the file, left the (genuinely fake) plugin's effect out entirely, and told me so on stderr. If your repository leans on an actual, obscure Prettier plugin that isn't Tailwind, package.json sorting or Svelte, bun fmt will tell you plainly that it can't help, rather than quietly doing the wrong thing. It's only the plugins it has decided to clone that create the risk, and that's a small, named list you can check with --doctor before you trust it on a real project.

For what it's worth, the "and more" in Bun's claim to format "JavaScript, TypeScript, CSS, HTML, JSON, YAML, Markdown and more" held up on the small CSS, Markdown and YAML files I threw at it: messy spacing, inconsistent list markers and sloppy indentation all came out correctly formatted in the same 2ms run. I didn't find anything to push back on there.

Concurrency and memory

bun fmt --check on Vite's source tree (1,589 JS/TS files, a different project that uses oxfmt rather than Prettier as its own formatter) took 0.09s wall time with 4 threads against 0.29s with --threads=1, a roughly 3.2x speedup on this machine's 4 cores. It degraded to single-threaded cleanly rather than failing, and the CPU time Bun reported (0.21-0.33s of user time against 0.10-0.15s of wall time on the Prettier repo check) confirms it's genuinely spreading work across cores rather than just being a fast single-threaded binary.

The memory gap was the largest number I measured anywhere in this test: 48MB peak for bun fmt against 519MB for Prettier, on the same 2,623-file check. Formatting is not usually a workload anyone worries about memory for, but on a CI runner with a tight memory limit, a tenfold difference like that is the kind of thing that turns a flaky job reliable.

bun fmt --check --timing on the Prettier checkout gives a phase breakdown rather than a single number, which is the closest thing to a production-facing metric this tool has:

wall: 34.4ms finding files and configs, 65.0ms formatting, 130.7ms total, 4 threads
Enter fullscreen mode Exit fullscreen mode

Finding and parsing configs costs more than a quarter of the total time on a repository this size, before a single file gets formatted, which is worth knowing if you're tempted to shave milliseconds off a CI step that runs on every push.

Run it yourself

curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"

git clone --depth 1 https://github.com/prettier/prettier.git
npm install -g prettier

# Count what bun fmt will actually check
bun fmt --list-files --cwd ./prettier | wc -l

# Time each tool, pointed correctly at the checkout
time bun fmt --check --cwd ./prettier
time prettier ./prettier --check --ignore-path ./prettier/.prettierignore

# See which plugins bun fmt has reimplemented natively
bun fmt --doctor --cwd ./prettier
Enter fullscreen mode Exit fullscreen mode

I ran exactly these commands, from outside the prettier directory, to get the numbers above.

What to do with this

If you're running Prettier in CI purely to check formatting, not to apply plugin-specific transforms you'd notice the absence of, bun fmt --check is very likely a safe, much faster swap, and the memory saving alone is worth it on a constrained runner.

If your .prettierrc lists any plugin at all, run bun fmt --doctor against your actual project before switching, and compare its "Built-in plugins" list against what your config names. Anything not on that list gets an honest refusal you can act on. Anything that is on the list will format successfully and silently, using Bun's own idea of what that plugin does, which may not be the one you pinned in package.json.