← Back to all posts

EF Core: Compiled Queries

EF Core: Compiled Queries

Reading Time: 3 minutes

Problem

In high-throughput APIs, some EF Core queries run thousands of times per minute with the same shape but different parameters.

Even though database execution is usually the biggest cost, EF still does query pipeline work on each execution, including expression processing and cache lookup.

On hot paths, that overhead can become visible:

  • higher CPU usage on app servers
  • lower request throughput under load
  • extra latency in endpoints that execute small, frequent queries

Teams often optimize SQL and indexing but miss this application-side overhead.

Solution

Use EF Core compiled queries for frequently executed, stable query shapes.

Compiled queries precompile the LINQ query into a delegate, reducing per-call query compilation overhead.

Best use cases:

  • very hot read paths
  • simple, stable query shapes
  • latency-sensitive endpoints

Not ideal when:

  • query shape changes dynamically
  • the query runs infrequently
  • database/network cost dominates so much that app-side savings are negligible

Description

1) Standard query (baseline)

public async Task<User?> GetByEmailAsync(AppDbContext db, string email, CancellationToken ct)
{
    return await db.Users
        .AsNoTracking()
        .FirstOrDefaultAsync(x => x.Email == email, ct);
}

This is perfectly valid and should be your default starting point.

2) Compiled async query

using Microsoft.EntityFrameworkCore;

public static class UserQueries
{
    private static readonly Func<AppDbContext, string, IAsyncEnumerable<User>> _userByEmailCompiled =
        EF.CompileAsyncQuery((AppDbContext db, string email) =>
            db.Users
              .AsNoTracking()
              .Where(x => x.Email == email)
              .Take(1));

    public static async Task<User?> GetByEmailCompiledAsync(AppDbContext db, string email, CancellationToken ct)
    {
        await foreach (var user in _userByEmailCompiled(db, email).WithCancellation(ct))
        {
            return user;
        }

        return null;
    }
}

The query shape is compiled once and reused, which can reduce overhead in hot paths.

3) Compiled query returning projection

Compiled queries work especially well with read-model projections:

public sealed record UserListItemDto(Guid Id, string Name, string Email);

public static class UserListQueries
{
    public static readonly Func<AppDbContext, int, int, IAsyncEnumerable<UserListItemDto>> PagedUsers =
        EF.CompileAsyncQuery((AppDbContext db, int skip, int take) =>
            db.Users
              .AsNoTracking()
              .OrderBy(x => x.Name)
              .Skip(skip)
              .Take(take)
              .Select(x => new UserListItemDto(x.Id, x.Name, x.Email)));
}

This combines two good performance practices:

  • projection (fetch only required columns)
  • compiled query delegate reuse

4) Practical guidelines

  • Measure first with realistic load tests.
  • Compile only a small set of truly hot queries.
  • Keep query shapes stable and parameterized.
  • Store compiled delegates as static readonly fields.
  • Prefer AsNoTracking for read-only scenarios.

5) Common mistakes

  • Compiling every query “just in case”
  • Using compiled queries for highly dynamic filters
  • Expecting large gains when database time dominates
  • Skipping benchmarking and attributing gains incorrectly

Summary

Compiled queries are a targeted optimization in EF Core.

They are most useful when the same query shape executes frequently and app CPU overhead matters. In those scenarios, compiled delegates can improve throughput and trim endpoint latency.

Use them selectively, benchmark before and after, and keep the rest of your query design fundamentals strong: indexing, projection, no-tracking reads, and efficient SQL.

References