We just passed the six-month mark since our team completed the full migration from our ESLint/Prettier setup to Ruff for Python code formatting and linting. I wanted to share some concrete performance and workflow observations, as the initial hype around speed is one thing, but long-term impact on developer experience is another.
The raw numbers are still impressive. Our pre-commit hook runtime for linting and formatting a moderately sized codebase (~150k lines) dropped from an average of 8-12 seconds to consistently under 0.8 seconds. That near-instantaneous feedback has genuinely changed how we work; there's no longer a mental penalty for running the linter. It's just part of the save action.
Beyond speed, the consolidation has been a major win for us. Managing a single `pyproject.toml` for both formatting and linting rules, instead of separate ESLint/Prettier configs, has simplified onboarding and reduced configuration drift. The built-in rule configuration is excellent, and we've found its defaults to be sensible with minimal overrides needed. We did have to spend time mapping our old ESLint rule equivalents (like our preference for `snake_case` in certain contexts) to Ruff's rule codes, but the documentation made this straightforward.
One area we monitored closely was IDE integration. The Ruff VS Code extension has been rock-solid, providing real-time highlighting and fix actions that feel just as responsive as the CLI. The only minor adjustment was getting the team to use `ruff check --fix` instead of the old ESLint fix command, which was a trivial change.
From a procurement and TCO perspective, the move has been positive. We eliminated two dependencies (and their transitive updates) for a single, actively maintained tool. The reduced CI time has shaved a small but non-zero amount off our cloud compute costs. The main "cost" was the migration effort itselfβabout two developer days to audit and translate our rule set.
For teams considering a similar move, my advice is to run Ruff in parallel on your codebase for a sprint, use its `--fix` aggressively, and then evaluate the remaining diffs. The performance gains are real, but the real value for us has been in the simplified toolchain and the elimination of that frustrating wait.
β Kat
read the contract
Hi, I'm Julie. I'm a platform lead at a 100-person SaaS company where we manage dozens of Python microservices. We standardized on Ruff for all new projects about a year ago and still maintain some legacy services with pre-existing ESLint configs.
**Integration & Maintenance Effort**: The setup and migration effort is low, but the actual work is in rule mapping. For a team of our size, standardizing across all projects took about two engineering weeks, mostly auditing our old ESLint rules (like `camelcase` and `prefer-const`) to find their Ruff equivalents or deciding to drop them. The ongoing maintenance is effectively zero.
**Performance & Developer Experience**: The speed claim is real and transformative for local dev. Our CI linting stage is consistently 5-10x faster across the board. The biggest win isn't the raw number, but the elimination of context switching. When the check is sub-second, developers run it constantly.
**Where It Clearly Wins**: For pure Python projects, Ruff's unification of linting and formatting with a single, coherent configuration file (`pyproject.toml`) is its killer feature. It removes toolchain debates and configuration sync issues. It's also a clear win for monorepos where you need consistent, fast checks across many directories.
**Where It Has Limitations**: It's a Python-only tool. If you have a full-stack codebase with TypeScript/JavaScript, you're now managing two separate linters (Ruff for Python, something else for JS). This can split the developer mental model and CI configuration. Also, while Ruff's rule set is extensive, some highly specific ESLint plugin rules for frameworks like React simply don't have a direct equivalent.
I'd recommend Ruff for any team working primarily in Python that wants to reduce toolchain complexity and get instant feedback. For a full-stack team, the decision hinges on whether you value language-specific optimization over a unified linting experience. Tell us if you have significant JS/TS code and how much you value a single configuration style across all your languages.
Read the guidelines before posting
Totally agree about the elimination of context switching being the real win, not just the raw speed number. It's shifted the whole dynamic for my team.
That's a really interesting point about rule mapping taking two engineering weeks for auditing. We found a similar hidden time sink, but ours was in convincing the last holdouts to let go of a handful of hyper-specific, legacy ESLint rules that didn't have a direct Ruff equivalent. We eventually just archived those rules in a doc and moved on, and honestly, nobody has missed them in six months. The consolidation into `pyproject.toml` was worth that small fight.
βjr
> "no longer a mental penalty for running the linter"
Right, because the 8 seconds you were saving by not running a linter was really the bottleneck in your day. Not code review, not debugging, not the 15-minute meeting about nothing.
I'll bite though. The 0.8 seconds - is that cold cache or warm? Because I've seen Ruff's first-run after a `git stash` or a fresh clone still take a couple seconds to warm up. And if you're running it on save with a hot cache, that's not the same as a CI run on a fresh checkout.
Also, "single pyproject.toml" sounds great until you have to debug why a rule isn't firing because of some nested config inheritance or a stray `.editorconfig`. I've spent more time untangling tool configs than I ever did fixing actual lint errors.
But hey, if it makes you feel fast, go with it.
-- old school
That consolidation from multiple configs to a single `pyproject.toml` is the quiet killer feature. We saw the same thing - it eliminated the whole "wait, is that rule coming from `.prettierrc` or `.eslintrc.json`?" dance during PR reviews.
The mental penalty thing is real, even if it sounds small. A 12-second lag meant people would batch changes and then get hit with a wall of lint errors. Sub-second feedback means they fix one or two things as they go. It's less about raw time saved and more about keeping flow state. 😄
That said, I hope you pinned your Ruff version. The defaults are sensible *now*, but they've been known to change between minor releases, which can cause some surprise CI failures.
Great point about the rule mapping being the real time sink. We had a similar audit process and ended up creating a simple spreadsheet to track our old ESLint rules, their Ruff equivalent (or lack thereof), and the team's vote on whether to keep, change, or drop it. That doc actually became a useful reference later for onboarding.
Your comment on eliminating context switching hits home. It's not just about speed, it's that the feedback is now immediate enough to feel like part of the editor itself. Developers stopped seeing it as a separate "check" step.
Your observation about the mental penalty is spot on, and I think the key metric you've identified is latency under load, not just the raw wall-clock time. When a tool's feedback loop drops below roughly one second, the cognitive overhead of invoking it disappears. The developer no longer makes a conscious decision to "run the linter"; it becomes a background process like syntax highlighting.
That said, I'd be curious about the variance in those 0.8 second runs. Are you measuring on developer workstations with varied hardware, or on a controlled CI runner? The performance consistency matters just as much as the median. A tool that's 0.8 seconds 90% of the time but spikes to 3 seconds occasionally can still disrupt flow, whereas a predictable 1.1 seconds is often preferable.
The config consolidation is a significant secondary win. Reducing the number of network round trips (so to speak) for configuration lookup eliminates a whole class of "why isn't this working?" debugging sessions. It's analogous to reducing DNS lookques in a web request chain.
Every microsecond counts.
> "latency under load, not just the raw wall-clock time"
A fair point, but I think you're giving the average dev too much credit. The "predictable 1.1 seconds" you mention is still an eternity if the underlying rule is a subjective style nit. The real disruption to flow comes from getting a ding for something trivial, not the latency of the ding.
The variance question is a good one, though. Everyone's reporting these sub-second wins, but is anyone actually tracking the p99 on a fleet of developer machines? Or are we all just feeling the placebo effect of a shiny new tool? I'd bet my coffee the spikes happen more often than anyone admits, but they get lost in the general relief of ditching the old setup.
Trust but verify
That "mental penalty" point really resonates. We saw the exact same shift, where the sub-second feedback made linting feel like a real-time code review buddy instead of a gatekeeper. It went from "ugh, I should run the linter before I commit" to just automatically fixing things as I type.
Your point about minimal overrides is key. We found that trusting Ruff's defaults on 90% of things actually improved consistency across our team, more than our old, heavily-customized ESLint config ever did. The time saved arguing over rule tweaks alone was worth the switch 😅
Have you run into any issues with auto-fix? We love it, but there are a couple of edge-case rules where the fix actually broke the code, so we had to disable auto-fix for those.
Less hype, more data.