I once spent half a day chasing a performance bug in an API endpoint. It was a simple read operation, but it was inexplicably slow and chewing up memory under load. The culprit? A single, innocent-looking Entity Framework Core default that was tracking thousands of objects for changes that would never happen.

EF Core is fantastic, but its defaults are designed for getting-started tutorials, not production applications. They prioritize ease of use over performance and safety. Here are the five settings I change in every single production project before writing a single line of business logic.

1. The Silent Killer: QueryTrackingBehavior.TrackAll

By default, every time you query for data, EF Core’s DbContext assumes you’re going to change it. It loads the entities and sets up change tracking, which consumes memory and CPU cycles to keep an eye on every property.

This is a complete waste for the 90% of queries that are read-only: fetching data for an API response, a report, or a UI display. I’ve seen this add 20-40% overhead on high-traffic endpoints. It’s the number one performance gotcha I find in code reviews.

The Fix: Turn it off globally.

// In your Program.cs or Startup.cs
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

This simple change makes all queries non-tracking by default. For the rare cases where you do need to fetch and then update an entity, you can explicitly opt in.

// Now you have to ask for tracking when you need it
var userToUpdate = await context.Users
    .AsTracking()
    .FirstOrDefaultAsync(u => u.Id == userId);

if (userToUpdate != null)
{
    userToUpdate.LastLogin = DateTime.UtcNow;
    await context.SaveChangesAsync();
}

This “opt-in” approach is safer, faster, and makes your code’s intent clearer.

2. The One-Liner That Can Wipe Your Database: DeleteBehavior.Cascade

This one burned me once in production. A seemingly harmless context.Remove(customer) call triggered a chain reaction that wiped out the customer’s orders, shipments, and support tickets. Thousands of records, gone. Why? Because EF Core defaults to cascade deletes for required relationships.

It’s a dangerous feature that makes it too easy to accidentally orphan or delete huge chunks of data.

The Fix: Set the default delete behavior to Restrict.

This forces the database to prevent a parent record from being deleted if any child records still point to it.

// In your DbContext's OnModelCreating method
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    // Set RESTRICT as the default delete behavior for all relationships
    foreach (var foreignKey in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
    {
        foreignKey.DeleteBehavior = DeleteBehavior.Restrict;
    }
}

My rule is simple: never trust default cascade deletes. If I need to delete an entity and its children, I do it explicitly in my business logic. That way, I can add logging, checks, and be 100% sure about what’s being removed.

3. The Performance Hog: nvarchar(max) for All Strings

When EF Core creates a migration for a string property, it defaults to nvarchar(max). This seems safe—no more exceptions because a user’s name was too long!

But in the database world, (max) is a performance red flag. It tells SQL Server, “I have no idea how big this data is,” which prevents it from creating efficient query plans. More importantly, you often can’t create an index on an nvarchar(max) column, leading to slow table scans instead of fast index seeks.

The Fix: Always specify a MaxLength.

Be realistic. An email address isn’t going to be 2,000 characters long. A postal code has a well-defined length.

// In your entity configuration or OnModelCreating
modelBuilder.Entity<User>(entity =>
{
    entity.Property(u => u.Email)
          .HasMaxLength(256) // A reasonable max for emails
          .IsRequired();
          
    entity.Property(u => u.FirstName)
          .HasMaxLength(100)
          .IsRequired();
});

Make this a non-negotiable part of your code reviews. Every string property should have a HasMaxLength attribute. It’s data validation at the database level.

4. The Round-Trip Nightmare: Default Batch Size

When you call SaveChangesAsync to insert, update, or delete 10,000 records, EF Core doesn’t send 10,000 separate commands. It batches them. But the default batch size for SQL Server is a surprisingly small 42.

For heavy data import or bulk processing jobs, this default creates a ton of unnecessary network round-trips to the database, slowing everything down.

The Fix: Increase the batch size for your workload.

I was working on a data migration script that felt sluggish. By increasing the batch size, I cut the execution time by over 60%.

// In your Program.cs or Startup.cs
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        sqlOptions.MaxBatchSize(100); // Start with 100 and test
    }));

Don’t go crazy here. A massive batch size can lead to command timeouts or hit SQL Server’s parameter limits. I usually start with 100 and tune it based on performance testing for a specific workload.

5. The Ghost of Client-Side Evaluation

This is less of a default to change and more of a “ghost” to be aware of. In older versions of EF Core (before 3.0), if you wrote a LINQ query that couldn’t be translated to SQL, EF would silently fetch entire tables into memory and then perform the filter on the client. It was a ticking time bomb.

Thankfully, since EF Core 3.0, this behavior now throws an exception by default. This is the correct behavior. Don’t ever turn it off.

The “Fix”: Leave the default alone and listen for the exceptions.

Ensure you have proper logging in place to catch these exceptions during development.

// In your Program.cs or Startup.cs, during development
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Warning) // See translation warnings
           .EnableSensitiveDataLogging()); // See parameter values

If you see an InvalidOperationException about a query that couldn’t be translated, it’s a bug in your code, not EF Core. Rewrite your query so it can be fully translated to SQL.

My Takeaway

Here’s my rulebook for a new EF Core project:

  • Always use UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking). This is the single biggest performance win for read-heavy apps. I only opt into tracking when I’m about to call SaveChanges.
  • Always disable cascade deletes globally. Deletes should be explicit and handled in your application code, not by a magic database default. It’s just safer.
  • Never use nvarchar(max) unless you are absolutely storing a novel. For everything else, HasMaxLength is mandatory.
  • Consider tuning MaxBatchSize for write-heavy applications. The default is too conservative for bulk operations.

These aren’t just suggestions; they are battle-tested rules from years of building and maintaining production systems with EF Core. They’ll save you from slow queries, accidental data loss, and late-night debugging sessions.

References

FAQ

Why should I override EF Core defaults?

EF Core defaults are designed for simplicity and demos, not production performance. Overriding them can reduce memory usage, prevent data loss, and improve query speed.

What is QueryTrackingBehavior in EF Core?

QueryTrackingBehavior controls whether EF Core tracks entities for changes. Disabling tracking for read-only queries significantly boosts performance.

Is DeleteBehavior.Cascade safe to use in EF Core?

Cascade delete can lead to accidental mass data loss if used without caution. It’s safer to set DeleteBehavior.Restrict and handle deletions explicitly.

How do I improve EF Core batch insert performance?

Increase MaxBatchSize in your DbContext configuration to reduce database round trips during bulk inserts, but always test against your workload.

Why avoid nvarchar(max) in EF Core?

Using nvarchar(max) prevents efficient indexing, slows queries, and leads to poor execution plans. Always set realistic maximum string lengths.

About the Author

@CodeCrafter is a software engineer who builds real-world systems , from resilient ASP.NET Core backends to clean, maintainable Angular frontend. With 11+ years in production development, he shares what actually works when shipping software that has to last.