---
title: 'Introducing Test Budget: a linter for test performance'
teaser: Your test suite didn't get slow all at once. It won't get fast all at once
  either. Start by making sure it stops getting worse.
tags: testing,performance,productivity
author: Matheus Richard
published_on: 2026-03-17
---

No one sets out to write a slow test, and yet it happens. A [test matcher that
burns through a full Capybara timeout][capybara-timeout]. An [innocent-looking
factory that cascades into dozens of records][too-many]. One test at a time,
your suite gets slower. By the time you notice, CI takes hours and nobody wants
to touch it.

**Having tests is not enough. We need _fast_ tests.** If tests are slow,
developers won't run them locally. When only CI runs the full suite, [feedback
loops stretch from seconds to hours][testing-rails].

## A linter for test performance

Without visibility, performance will always be an afterthought. We have no
shortage of performance monitoring tools for production, but very few for tests.

I built [Test Budget][test-budget] to fill that gap. It reads your test run
timings and reports when they exceed a configured budget. It doesn't change how
your tests run. It just tells you when they're too slow (before it gets worse).

You can set budgets at two levels:

- **Suite-level**: your entire test suite should run in under 10 minutes, for example.
- **Per-test-type**: model tests should run in under 1.5 seconds, system tests
  under 10 seconds, and so on.

## Estimating and enforcing your first budget

You don't have to pick numbers from thin air. Test Budget can generate a starter
config from your existing test results. First, run your tests with RSpec's JSON
formatter to produce a timings file:

```bash
bundle exec rspec --format json --out tmp/test_timings.json
```

Then generate a starter config from those results:

```bash
bundle exec test_budget init tmp/test_timings.json
```

It derives budgets from your actual data:

- The **total suite budget** is set 10% above your current total, giving you
  headroom to keep adding tests without immediately blowing the budget.
- The **per-test-type budgets** are based on the 99th percentile. Your slowest
  1% of tests are the ones you need to fix. The rest already pass.

These are starting points. The generated config is a plain YAML file you can
tune to match your team's standards.

Then run the audit after your tests:

```
bundle exec test_budget audit

Test budget: 1 violation(s) found

  1) spec/system/signup_spec.rb -- creates account (11.20s) exceeds system limit (6.00s)
```

The audit tells you exactly which tests to look at. From there, you can make
them faster. The README has [a list of strategies to use in these
cases][violations].

## Allowlisting is temporary by design

Like [RuboCop's generated todo][todo], Test Budget has an allowlist. Instead of
bumping the budget to accommodate a slow test, you allowlist it with a reason.
This keeps the budget honest for everything else and makes it clear which tests
need attention.

```yaml
allowlist:
  - test_case: "spec/services/invoice_pdf_spec.rb -- generates PDF with line items"
    reason: "PDF generation is inherently slow, tracked in #1234"
    expires_on: "2026-04-01"
```

Allowlist entries have an expiration date. When they expire, the test starts
failing the audit again. This creates friction on purpose. It keeps entries
from quietly overstaying their welcome and encourages you to fix the underlying
issue instead of sweeping it under the rug.

## You can't afford slow tests

The right budget depends on your team and project, but I'd encourage you to be
aggressive. **[Way too many teams][slow-suites] are comfortable with 10+ minute
test suites.** That is an awful lot of time to wait for feedback. Fast tests are
one of the best things you can do for developer productivity.

The goal isn't zero violations on day one. It's to stop overspending and make
test performance visible. Start with what you have, then work on improving it.

Give [Test Budget][test-budget] a try:

```bash
bundle add test_budget
bundle exec rspec --format json --out tmp/test_timings.json
bundle exec test_budget init tmp/test_timings.json
bundle exec test_budget audit
```

[capybara-timeout]: https://thoughtbot.com/blog/combine-capybara-selectors-to-avoid-the-sequential-timeout-trap
[too-many]: https://www.youtube.com/watch?v=LOlG4kqfwcg
[todo]: https://docs.rubocop.org/rubocop/latest/usage/auto_gen_config.html
[test-budget]: https://github.com/thoughtbot/test_budget
[violations]: https://github.com/thoughtbot/test_budget#i-have-violations-now-what
[slow-suites]: https://x.com/tomasz_wro/status/1348568856181288962
[testing-rails]: https://books.thoughtbot.com/assets/testing-rails.pdf
