Home
All articles
PerformanceEF Core.NET

Cutting API Latency: Lessons from EF Core in Production

May 2, 20266 min read

Every team eventually meets the endpoint that used to be fine. It worked great in the demo, and now it takes two seconds under real data. Nine times out of ten the fix isn't a bigger database — it's changing how the query is written.

Kill the N+1 before it kills you

The classic trap: load a list, then lazily touch a navigation property inside a loop. One query becomes a hundred. EF Core will happily let you do this, and it will happily be slow.

csharp
// Slow: one query for loans, then one per loan for the borrower.
var loans = await db.Loans.ToListAsync();
foreach (var loan in loans)
    Console.WriteLine(loan.Borrower.Name); // lazy round-trip each time

// Fast: ask for what you need up front.
var loans = await db.Loans
    .Include(l => l.Borrower)
    .AsNoTracking()
    .ToListAsync();

Project to what the screen needs

A read endpoint almost never needs the whole entity. Selecting into a small DTO means less data over the wire, no change-tracking overhead, and a query the database can often satisfy from an index alone.

  • Use .Select() to shape exactly the columns the response returns.
  • Add .AsNoTracking() to every read-only query — the change tracker is pure cost you don't need.
  • Paginate at the database with .Skip()/.Take(), never in memory after loading everything.

Measure, don't guess

Before optimizing anything, log the generated SQL and time it. Half the time the query is fine and the real cost is somewhere else — serialization, a chatty client, an unindexed filter. The profiler is honest in a way our intuition is not.

The fastest query is the one you never send. The second fastest is the one that returns only what the caller will actually use.

None of this is exotic. It's just discipline applied consistently — and consistency is what turns a p95 from two seconds into two hundred milliseconds.