Sitemap
Kaushal Gangwar

Kaushal Gangwar

Software Engineer • Gadget Enthusiast • DevEx Advocate. Building software, exploring tech, and sharing ideas to help developers work smarter.

From 55-Minute Lint Job to 10: Building a Feedback Loop Fit for Agents

7 min readJun 17, 2026

--

This is the first in a series on how we sharpened our feedback loops, both to make everyday development feel effortless and to get our codebase ready for a world where agents help write and verify it. We start with the change that paid off the fastest: linting

Press enter or click to view image in full size
Push Code. Wait. Wait some more.

There was a running joke on our team. If you wanted a coffee, you pushed your branch first. The lint job would still be running when you got back.

It took 55 minutes. Fifty-five minutes for a machine to tell you that you had imported something in the wrong order. By the time it finished, you had already context-switched to something else, forgotten what the change even was, and half the time it failed on a rule you could have fixed in two seconds if only you had seen it earlier.

And seeing it earlier was the real problem, because you could not run lint locally at all.

Every time someone tried, their terminal would grind for a minute or two, the laptop fan would spin up like it was about to take off, and then this would appear:

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

So nobody ran it locally. Which meant problems were only ever caught on CI. Which was slow. Which made the feedback loop miserable. It was a quiet little doom loop, and we had all just gotten used to living inside it.

Why a slow loop suddenly mattered more

Humans are remarkably good at hiding slow feedback from themselves. We batch our work, we context-switch, we go make coffee. A 55-minute loop is awful, but a human can route around it.

An agent cannot.

As we started seriously thinking about agentic development, where an AI agent writes code, runs the checks, reads the result, and tries again, one thing became obvious. An agent lives or dies by the speed and reliability of its feedback loop. It does not batch work to pass the time. It needs to know, quickly and deterministically, whether the change it just made was good. If the only way to find out is a 55-minute CI job that cannot even run locally, the agent is either useless or ruinously expensive.

The realization reframed the whole problem. Fast, trustworthy feedback was no longer a nice-to-have for developer happiness. It was the foundation any agentic workflow would have to stand on. And our foundation was cracked.

So we decided to fix it, starting with the slowest, most broken loop we had.

Can’t we just use something faster?

This is the part where, today, someone would say “just switch to one of those Rust based linters.”

But this was before tools like oxlint were publicly available. The fast, native linters that everyone reaches for now simply were not an option yet. There was no escape hatch to a different tool. If lint was going to get faster, we were going to have to make ESLint itself faster. We could not run away from the problem. We had to actually understand it.

So we sat down and asked the question we should have asked months earlier: where is all the time actually going?

Press enter or click to view image in full size
Time to find out where 55 minutes was actually going.

The thing nobody had noticed

Here is what we found, and it reframed everything.

ESLint rules for a TypeScript codebase quietly fall into two very different families.

The first family only cares about the shape of a single file. Import order, naming, unused variables, React hooks rules, most stylistic checks. To run these, ESLint just reads the file, parses it into a tree, and inspects that tree. It does not need to know anything about any other file in the entire repo. These rules are cheap and fast.

The second family needs to understand the types flowing through your code. Rules like “you forgot to await this promise” cannot be answered by looking at one file. To know whether something is a promise, ESLint has to ask the TypeScript compiler, and TypeScript can only answer by building a complete map of every type in the whole project. Building that map is about as expensive as running the type checker over your entire codebase. In a large monorepo, it is enormous. It costs minutes of work and gigabytes of memory.

Now here is the part that stung.

The moment you switch on even one of those type-aware rules, you have to point ESLint at your TypeScript config. And the instant you do that, every single rule has to wait for that giant type map to finish building first. Including all the cheap, fast rules that never needed it.

Get Kaushal Gangwar’s stories in your inbox

Join Medium for free to get updates from this writer.

Our quick syntactic rules, the ones that could have run in seconds, were standing in line behind a multi-minute type checking process for no reason at all. And that same process was the thing devouring memory and killing laptops.

We were paying the full price of type checking just to lint an import statement.

The fork in the road

Two kinds of rules. They never needed to travel together.

Once we saw it that way, the fix almost designed itself. The expensive work was only needed by a small minority of rules. The majority were being dragged along for the ride.

So we stopped running one lint command that did everything slowly, and split it into two passes that ran independently.

The first pass handles all the cheap, syntactic rules. The key detail is what it leaves out. It deliberately does not tell ESLint about the TypeScript project, which is exactly what stops the giant type map from ever being built.

The second pass is the slow one. It opts into the type map on purpose, and it carries only the handful of rules that genuinely need it.

On CI, the two passes stopped being one serial monster and became two jobs running side by side. And locally, something wonderful happened. Developers could finally run syntactic lint on their own machines. With no type map to build, it did not touch enough memory to crash, and it came back in seconds.

From the split alone, our CI lint time dropped from around 55 minutes to about 15 minutes.

That was already a different world. But there was one more win sitting right there.

The cache that finished the job

Press enter or click to view image in full size
From a coffee break to a few seconds.

ESLint has a built-in cache. It remembers the result for each file, keyed on the file’s contents. On the next run, anything that has not changed gets skipped.

So the cache was never simply “switch it on for the fast pass.” It was “switch it on for the rules that are genuinely file-local, and keep every cross-file rule out of the cached path,” leaning on the type checker for the ones, like a renamed export, that it catches anyway. Once we drew that line carefully, we turned the cache on and kept it warm between runs. After that, only the files someone had actually touched got re-linted. Everything else was skipped instantly.

With the cache kept warm between runs, only the files you actually touched got re-linted. Everything else was skipped instantly.

That took syntactic lint from around 15 minutes to under 2 minutes on CI, and locally, repeat runs became nearly instant.

The coffee-break joke died. Nobody missed it.

One trap we are glad we spotted

There is a tempting next step that will quietly burn you, so it is worth saying out loud. Do not slap the cache onto the type-aware pass too.

The cache assumes a file’s result depends only on that file. For type-aware rules, that is simply not true. If you edit file A, you can change the correct lint result of file B, because B imports a type from A. But B’s own contents have not changed, so the cache happily skips it and reports a stale, wrong answer.

So the rule is short and firm. Cache the syntactic pass. Never cache the type-aware pass. The clean part of the split is that the cacheable rules and the dangerous-to-cache rules end up neatly separated by design, not by accident.

What it actually taught us

The strange thing is that the fix had almost nothing to do with ESLint config in the end.

We had spent months treating “lint is slow” as a fixed cost of the universe. It was not. It was one genuinely expensive operation, building the full type map, being forced onto a pile of rules that never asked for it. The entire 55 minutes, and every crashed laptop, came down to that one mismatch.

And because we could not just reach for a shiny faster tool, we were forced to understand the actual shape of the problem. In hindsight, that was the gift. A faster linter would have papered over the waste. Splitting the work made the waste impossible to ignore.

So if your lint is slow and your code is TypeScript, before you go shopping for a new tool, ask one question first. How much of your linting is sitting in line behind the type checker for no reason at all?

You might be one config split away from getting most of your day back.

--

--

Kaushal Gangwar
Kaushal Gangwar

Software Engineer • Gadget Enthusiast • DevEx Advocate. Building software, exploring tech, and sharing ideas to help developers work smarter.