LINQ Performance: When It's Slow and What to Do About It
LINQ is my favorite feature of C#. It turns intent-obscuring loops into declarative one-liners, and in 95% of code its overhead is irrelevant. But I've also profiled enough .NET services to know the other 5% — the places where innocent-looking LINQ chains quietly dominate a hot path.
This post is about recognizing those places, with the fixes I actually apply. No cargo-cult "LINQ is slow, always use for loops" advice — that's wrong, and unreadable code is its own performance problem (it slows down the developers).
First, understand deferred execution
A LINQ query over IEnumerable<T> doesn't do anything when you define it:
The work happens when you enumerate — in a foreach, a ToList(), a Count(). This is the root of both the best and worst LINQ behaviors, so every pitfall below comes back to it.
Pitfall 1: Multiple enumeration
Three enumerations, three executions of the pipeline. If the source is a database query, that's three round trips; if the predicate is expensive, you're paying it repeatedly. ReSharper and modern analyzers warn about this ("possible multiple enumeration") — take the warning seriously.
The fix is to materialize once when you know you'll consume the results more than once:
The judgment call: materializing costs one allocation of the whole result set. Enumerate-once pipelines (a single foreach over a filtered stream) should not be materialized. Consume-many pipelines should.
Pitfall 2: Count() when you mean Any()
Count() with a predicate must visit every element; Any() short-circuits. Same logic, potentially thousands of times less work. Related: prefer the Count property (lists, arrays) over the Count() method when you have a concrete collection — though LINQ is smart enough to use it under the hood for ICollection<T>.
Pitfall 3: Quadratic lookups hiding in plain sight
This is the most expensive one I find in real code:
10,000 orders × 10,000 customers = 100 million comparisons. The fix is a dictionary built once:
I've seen this single change take a report from 40 seconds to under one. Any time you see First/FirstOrDefault/Where with an equality predicate inside a loop, reach for ToDictionary, ToLookup, or a Join.
Pitfall 4: Ordering more than you need
Actually — this one is fine now. Since .NET 7, OrderBy(...).Take(k) is internally optimized into a partial sort. But two ordering mistakes still cost real money:
- Sorting before filtering.
OrderBy(...).Where(...)sorts elements you're about to discard; filter first. OrderBy(...).First()— replace withMinBy/MaxBy(available since .NET 6), which is O(n) instead of O(n log n).
Pitfall 5: Allocation pressure in hot paths
Every lambda that captures a local variable allocates a closure; every Select/Where allocates enumerator machinery; GroupBy buffers the entire source. None of this matters in a request handler that runs occasionally. All of it matters in a loop that runs 100,000 times per second.
In genuinely hot paths (measured, not guessed — use BenchmarkDotNet or the Visual Studio profiler):
My rule: write LINQ first, profile, then rewrite the top offenders as loops with a comment explaining why. A codebase of preemptively "optimized" loops is harder to maintain and usually no faster where it counts.
Pitfall 6: Forgetting you're talking to a database
IQueryable<T> looks like LINQ-to-objects but compiles to SQL — until something forces it into memory:
Keep filters, projections, and paging on the IQueryable side so they run in the database. Anything the provider can't translate will either throw (EF Core) or silently switch to client evaluation in older stacks — know which one your ORM does.
The mental checklist
- Will this sequence be enumerated more than once? Materialize it.
Any()overCount() > 0,MinBy/MaxByoverOrderBy().First().- Equality search inside a loop → dictionary or join.
- Filter before you sort; page before you materialize.
- Hot path proven by a profiler? A loop is allowed to win.
- On
IQueryable, keep the work in SQL.
LINQ isn't slow. Unexamined LINQ in the wrong place is slow — and now you know exactly where to look.
Where to go next
- Fixing the N+1 Query Problem in Entity Framework Core — the database side of query performance.
- Choosing the Right C# Collection — the data-structure decisions behind Pitfall 3.
- Async/Await in C#: 8 Common Mistakes — the other invisible performance killer.