What 3 Months of Biome in Production Taught Me About My Toolchain — And What I'd Change
I wrote about switching from ESLint to Biome back in early May. The numbers were clean: 847 files, 31 seconds down to 2.3, one config file instead of three. It read like a victory lap.
Three months later — after Biome has linted every commit, survived a production incident, and been through two minor version bumps — the story is more nuanced. Some things held up better than I expected. A few things caught me off guard. And one mistake cost me an entire CI afternoon.
Here’s what actually happened after the migration hype faded.
The Numbers After 3 Months
When I wrote the original article, the benchmarks were from a single clean run. Real production tells a different story:
| Metric | Day 1 | After 3 Months |
|---|---|---|
| Lint + Format (847 files) | 2.3s | 1.8s |
| CI pipeline (lint step) | 14s | 9s |
| Config files managed | 1 | 1 |
| Rules enabled | ~180 | 210 |
| False positives reported | 12 | 3 |
tsc --noEmit still needed? | Yes | Yes |
Two things stand out. First, Biome got faster over time — the v2.3 and v2.4 releases added incremental analysis caching that shaved another 20% off our CI lint step. Second, the rule count grew because I gradually enabled rules I’d disabled during migration (import sorting, a few complexity checks). I was too conservative on day one.
The tsc --noEmit row is the one that hasn’t changed. Biome doesn’t replace TypeScript’s type checker, and it won’t. If your CI pipeline had tsc --noEmit before Biome, it still needs it after. That’s not a Biome problem — it’s the cost of using TypeScript.
What Surprised Me (In a Good Way)
1. The Import Organizer Became My Favorite Feature
I didn’t expect to care about import sorting. My old ESLint setup had eslint-plugin-import with a specific group ordering, and honestly, I’d forgotten it was even running.
Biome’s import organizer is different. It’s not a lint rule you enable — it’s an assist action that runs automatically on save. And it’s smart about grouping: Node built-ins first, then external dependencies, then internal packages, then relative imports. Empty lines between groups. No config needed.
{
"organizeImports": {
"enabled": true
}
}
That’s the entire config. Three months in, I’ve never once had an import ordering disagreement in a PR review. Before Biome, it was a weekly occurrence. This small thing has saved more review time than I expected.
2. CI Caching Works Better Than ESLint’s --cache
ESLint’s --cache flag always felt fragile to me. It missed edge cases, bloated the cache file, and occasionally ran full analysis when it shouldn’t have. Biome’s caching is built into the binary — no flag needed, no cache file to manage.
In our GitHub Actions pipeline, the lint step dropped from 14 seconds to 9 seconds after the first cached run. The cache key is based on file content hashes, so it invalidates correctly when files change. I checked the logs: on a PR that touched 12 files, Biome analyzed only those 12 files plus the ones they import. The rest were cached.
I didn’t benchmark this for the original article because I assumed ESLint’s caching was comparable. It’s not. Biome’s is cleaner and more predictable.
3. The GritQL Plugin System Changed My Mind About Custom Rules
When I wrote the original article, I listed “no custom rules” as a Biome limitation. That was true for Biome 2.0. Biome 2.4 shipped with GritQL — a pattern-matching language for writing custom lint rules without touching Rust.
We needed a rule that flagged console.log in any file under src/ (but allowed it in src/test/). The old ESLint version was a 30-line JavaScript rule. The GritQL version:
console.log($args) where {
$args <: _
} within file($path) where {
$path <: not "src/test/**"
}
Eight lines. No npm install, no plugin configuration, no version compatibility headaches. I ran biome check --apply and it flagged 47 console.log statements we’d missed in code review.
This changes the calculus significantly. The “no custom rules” objection that I acknowledged in the original article is no longer valid for most use cases. GritQL won’t cover every ESLint plugin scenario, but it covers the 80% that teams actually need.
The Mistake I Made (And What It Cost)
Here’s the embarrassing part. Two weeks after migrating, our CI pipeline started failing intermittently on the lint step. Sometimes it passed in 9 seconds, sometimes it timed out at 5 minutes. The error message was unhelpful: Biome process killed by OOM.
I spent an afternoon blaming GitHub Actions runners. Then I looked at the biome.json config I’d copied from the migration guide:
{
"files": {
"include": ["**/*"],
"ignore": ["node_modules", "dist"]
}
}
The **/* glob was matching our dist/ directory on some CI runs because the ignore list wasn’t being applied consistently across monorepo packages. Biome was trying to lint compiled JavaScript, source maps, and generated type definitions. On a clean run (fresh dist/), it was fine. On a dirty run (leftover dist/ from a previous job), it choked.
The fix was trivial — use the new workspaces config instead of a blanket glob:
{
"workspaces": ["packages/*"]
}
But it cost me two hours of debugging and a frustrated team. The lesson: Biome’s default file discovery is aggressive in monorepos. The roadmap acknowledges this — the 2026 plan specifically calls out monorepo auto-discovery as a past mistake and promises opt-in workspaces as the fix. I should have used workspaces from day one instead of trusting the defaults.
What Still Doesn’t Work (And When It Matters)
I want to be honest about the gaps, because the Biome ecosystem has grown since my original article and the trade-offs have shifted.
Vue and Svelte Template Support Is Still Partial
Biome can lint the <script> sections of .vue and .svelte files — the JavaScript and TypeScript parts. But it doesn’t lint the template markup (.html-like syntax inside those files). If your team uses Vue or Svelte, you’ll need to keep ESLint for template linting alongside Biome for JS/TS.
This creates a split toolchain: Biome handles 70% of your code, ESLint handles the remaining 30%. The mental overhead of maintaining two toolchains erodes the simplicity that made Biome attractive in the first place.
Does this matter? If you’re a Vue or Svelte shop, yes — stick with ESLint for now. If you’re React or plain TypeScript, this is irrelevant.
The Rule Count Gap Has Narrowed, But the Ecosystem Gap Hasn’t
Biome now has 450+ rules (up from ~300 when I wrote the original article). ESLint, with its plugin ecosystem, still has 700+ rules plus thousands of community plugins. The gap that matters isn’t the raw number — it’s whether the rules you need exist in Biome.
For our project, 210 rules cover everything we care about. But I’ve seen teams that depend on very specific plugins: eslint-plugin-react-hooks for React hook rules, eslint-plugin-security for security auditing, eslint-plugin-jest-dom for testing patterns. Some of these have Biome equivalents. Some don’t.
The practical test: Run biome migrate eslint --write on your project. If it migrates all your rules cleanly, you’re fine. If it skips rules you depend on, you need to evaluate whether those rules are essential or cargo-culted.
Markdown Support Still Isn’t Happening
The Biome 2026 roadmap explicitly lists Markdown as “paused due to resource constraints” and is actively seeking a volunteer champion. If you lint your Markdown files (and you should — broken links, spelling, heading hierarchy), you’ll need a separate tool.
This is a minor gap for most teams, but it’s one more tool in the chain. The dream of “one toolchain for everything” remains unrealized.
What the Biome Team Got Right Since I Migrated
The project’s velocity since Biome 2.0 has been impressive. Here’s what shipped in the three months I’ve been using it:
- v2.3: Incremental analysis caching (the thing that made our CI 20% faster)
- v2.4: GritQL plugin system for custom rules,
noImportCyclelint rule, enhanced monorepo debugging biome-ignore-all: File-level rule suppression (previously only line-level)- SCSS support started development: This was the most-requested feature on GitHub discussions
The team also corrected course on communication. The experimental Vue/Svelte/Astro announcement in early 2026 was framed as “coming soon” when it was really “experimental and incomplete.” They’ve since adopted more honest messaging about feature maturity. I appreciate that — it’s the kind of course correction that separates serious projects from hype.
What I’d Do Differently If I Started Today
Knowing what I know now, here’s my revised migration checklist:
- Use
workspacesfrom day one — Don’t trust the default file discovery in monorepos. Define explicit workspace boundaries. - Run
biome migrate eslint --writeand audit the skipped rules — Don’t just accept the migration. Review what was skipped and decide case by case. - Enable
noImportCycleimmediately — This catches circular imports that ESLint sometimes misses, and it’s now built into Biome. - Keep
tsc --noEmitin CI — Biome doesn’t replace type checking. Don’t remove it thinking Biome covers it. - Test GritQL for custom rules before assuming you need ESLint — The 2.4 release changed the custom rules calculus. Many teams don’t need ESLint plugins anymore.
The Decision Matrix (Updated)
Here’s my revised recommendation, three months and a production incident later:
| Scenario | Recommendation |
|---|---|
| Greenfield React + TypeScript project | Biome. No question. One config, fast CI, import organizer saves review time. |
| Existing project with <10 custom ESLint rules | Biome. Migration is straightforward. GritQL covers most custom rules. |
| Vue or Svelte project | Stay on ESLint (for now). Template linting gap is real. Revisit when Biome stabilizes HTML-ish languages. |
| Heavy custom ESLint plugin usage (5+ plugins) | Audit first. Run the migration, see what’s skipped. You might only need 1-2 plugins that justify keeping ESLint. |
| Monorepo with 3+ packages | Biome with workspaces config. Define explicit workspace boundaries. Don’t use default file discovery. |
| Solo developer or small team (1-3 devs) | Biome. The simplicity wins are biggest when there’s no dedicated tooling person to manage ESLint config drift. |
The core thesis from my original article holds: for most TypeScript projects, Biome is the right choice today. What’s changed is the nuance — I know where the edges are now, and I know what mistakes to avoid.
The Bottom Line
Three months in, Biome hasn’t just held up — it’s gotten better. The speed improvements from incremental caching, the GritQL plugin system, the honest course corrections on communication and monorepo support. The project feels like it’s maturing, not just shipping features.
But the migration wasn’t flawless. The CI OOM incident was entirely my fault — I trusted defaults I should have questioned. And the Vue/Svelte template gap is real for teams in those ecosystems.
My rule now is simple: if your project is JavaScript, TypeScript, JSX, JSON, CSS, or GraphQL — go Biome. If you need Vue/Svelte template linting, heavy custom ESLint plugins, or Markdown linting — evaluate the gaps before committing.
Sometimes the best toolchain is the one you stop thinking about. After three months, I’ve stopped thinking about Biome. It just works. And that, more than any benchmark, is the real win.
Enjoying the content? Here are tools I personally use and recommend:
- 🌐 Hosting: Bluehost — what this blog runs on
- 🛒 Tech Gear: My Amazon Store — keyboards, monitors, dev tools I use
Purchases through my links help keep this blog ad-free 💙
Enjoyed this post?
Subscribe to the newsletter or follow on YouTube for more dev content.
🎬 Watch Shorts