Does EF Core Query Compilation Lock Cause Latency Spikes Under Concurrent Requests?
0 reputation · 05 Sept 2024, 04:12 UTC
0 reputation · 05 Sept 2024, 04:12 UTC
To determine whether the internal query‑compilation lock in Entity Framework Core (EF Core) introduces unpredictable latency when multiple concurrent requests trigger new query shapes.
EF Core’s compilation cache is guarded by a per‑AppDomain lock. Under high concurrency, several threads may wait for the lock, potentially delaying the first execution of a query. Additionally, DbContext pooling, connection pooling, and default change‑tracking can interact with this contention in ways that are hard to isolate in a single‑threaded test.
31775 reputation · 05 Sept 2024, 11:59 UTC
Yes, the internal query-compilation lock in Entity Framework Core can cause measurable latency spikes when multiple concurrent requests trigger the first-time compilation of distinct query shapes. This contention serializes the compilation process, adding latency proportional to the number of contending threads. Once the cache is warmed, these spikes disappear.
EF Core utilizes a compilation cache to avoid the overhead of parsing expression trees and generating SQL for every request. This cache is guarded by a lock—a ReaderWriterLockSlim in EF Core 7, with more granular locking introduced in EF Core 8. When several threads simultaneously request query shapes that are not yet cached, they must wait for the lock to compile their respective queries.
Under high concurrency (e.g., 20+ threads) with many unique query shapes, p99 latency can increase by 5–50ms per contending thread. This is most observable during "cold starts," such as immediately after a deployment or an application pool recycle.
Move the compilation cost to application startup using EF.CompileQuery. This creates a thread-safe delegate that bypasses the compilation cache lock entirely during request execution.
// Example for EF Core 7/8
private static readonly Func<MyDbContext, int, IEnumerable<Order>> GetOrdersByCustomer =
EF.CompileQuery((MyDbContext ctx, int id) =>
ctx.Orders.Where(o => o.CustomerId == id));
// Usage in request
var orders = GetOrdersByCustomer(context, customerId);
Note: This is not applicable to dynamic LINQ queries where the query shape is built at runtime.
Using AddDbContextPool allows the compilation cache to be shared and warmed more efficiently. However, if the pool size is smaller than the peak concurrency, threads will wait for a free context before they even reach the compilation lock, compounding the latency. Ensure the pool size is scaled to match peak concurrent demand, while monitoring GC pressure.
To verify if the lock is the primary bottleneck, use the following scoped approach:
EF.CompileQuery.dotnet-trace or PerfView to look for System.Threading.ReaderWriterLockSlim wait events within the Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler namespace.LogLevel.Debug to correlate "Compiling query model" entries with specific thread IDs.To refine this recommendation: Which version of EF Core are you using (7.x vs 8.x), and are your latency spikes occurring with static query shapes or dynamically generated LINQ expressions?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.