Reducing Latency in High-Throughput Reads with EF Core Compiled Queries
Learn how to use EF Core Compiled Queries to eliminate LINQ-to-SQL translation overhead in high-throughput services, including a worked example and performance trade-offs.
01 Sept 2025, 13:53 UTC

The Hidden Cost of LINQ Translation
In a high-traffic .NET service, a single database query might execute in 5ms, but the process of turning a LINQ expression into SQL can consume a significant slice of your CPU budget. Every time EF Core encounters a new LINQ query, it must parse the expression tree, map it to the database provider's SQL dialect, and cache the resulting execution plan. While EF Core has a built-in query cache, the overhead of checking that cache and managing the expression tree for every request adds up in read-heavy environments.
When a specific query—such as fetching a user profile or an order by ID—is called thousands of times per second, the most effective way to reduce this per-request overhead is to move the translation step from the request pipeline to the application startup using Compiled Queries.
How Compiled Queries Work
Compiled queries allow you to pre-compile the translation from a LINQ expression to SQL. Instead of defining the query inside a method body where it is evaluated at runtime, you create a delegate using EF.CompileAsyncQuery. This delegate acts as a reusable function: the SQL is generated once and stored, and subsequent calls simply pass new parameters to that pre-baked command.
This is an engineering decision based on the 80/20 rule. You don't compile every query in your application—that would bloat startup time and complicate the code. Instead, you identify the "hot paths" (the few queries that make up the bulk of your traffic) and optimize only those.
Implementation Example: High-Frequency Lookup
Consider a scenario where a service frequently retrieves an Order and its associated LineItems by an ID. In a standard repository, this is often written as a method that builds the query on every call.
Below is the pattern for implementing a compiled query. This example assumes EF Core 6.0+ and a standard DbContext.
public class OrderRepository
{
private readonly MyDbContext _context;
// 1. Define the compiled query as a static readonly delegate
// This translation happens once per application lifetime
private static readonly Func<MyDbContext, int, Task<Order?>> _getOrderById =
EF.CompileAsyncQuery((MyDbContext context, int id) =>
context.Orders
.AsNoTracking() // Essential for read-heavy performance
.Include(o => o.LineItems)
.FirstOrDefault(o => o.OrderId == id));
public OrderRepository(MyDbContext context)
{
_context = context;
}
public async Task<Order?> GetOrderAsync(int orderId)
{
// 2. Execute the delegate by passing the context and parameters
return await _getOrderById(_context, orderId);
}
}
Key Configuration Details
- Permissions: The code runs with the permissions of the application identity executing the
DbContext. - AsNoTracking(): Because compiled queries are typically used for high-performance reads, combining them with
AsNoTracking()removes the overhead of the EF Change Tracker, further reducing CPU and memory usage. - Placeholders: The
int idin the delegate is a parameter placeholder; EF Core ensures the resulting SQL uses parameterized queries to prevent SQL injection.
The Trade-off: Flexibility vs. Speed
Compiled queries are not a universal replacement for standard LINQ. They introduce a significant limitation: the query shape must be static.
| Feature | Standard LINQ | Compiled Queries |
|---|---|---|
| Dynamic Filtering | Supported (e.g., adding .Where() based on if-statements) |
Not Supported (Expression must be fixed) |
| Compilation Cost | Per-request (cached) | Once at startup |
| Maintainability | High (Intuitive syntax) | Moderate (Requires delegate management) |
If your UI allows users to toggle five different filters, a compiled query cannot handle the permutations of those filters. In those cases, standard LINQ is the correct choice.
Verifying the Performance Gain
To confirm that the compiled query is actually bypassing the translation phase, you can use the following verification steps:
- Log Inspection: Enable
Microsoft.EntityFrameworkCore.Database.Commandlogging. In a standard query, you may see translation logs on the first few calls. With a compiled query, the translation occurs during the static initialization of the delegate, not during the request execution. - CPU Profiling: Use a tool like dotnet-trace or Visual Studio Profiler under a load test. Look for a reduction in time spent within
Microsoft.EntityFrameworkCore.Querynamespaces. - SQL Consistency: Verify in your database logs (e.g., SQL Server Profiler) that the generated SQL remains identical across different parameter values, confirming the plan is being reused.
Summary Checklist
Before implementing compiled queries, ensure the following conditions are met:
- The query is identified as a "hot path" via profiling.
- The query structure never changes based on runtime logic.
- You have applied
AsNoTracking()for read-only operations. - You are targeting a stable version of EF Core, as delegate signatures can vary between major releases.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.