Diagnosing EF Core SaveChanges Change-Tracking Conflicts
How to tell whether SaveChanges failed because of duplicate tracked instances, concurrency tokens, or database constraints—and what to fix.
13 Apr 2026, 03:18 UTC

The problem and what to check first
When SaveChanges() fails, the exception text often points to the database, but the real cause is frequently inside the DbContext change tracker. A context keeps track of every entity it has loaded or attached. If two tracked instances carry the same primary key, EF Core cannot decide which one represents the current row, and it throws before sending SQL to the database.
Useful takeaway: match the exception type to the tracker state before changing code. A concurrency token mismatch and a duplicate tracked instance look similar in a stack trace, but they require opposite fixes.
Recognizable condition
You are likely dealing with a change-tracking conflict when SaveChanges() throws one of these:
InvalidOperationExceptionmentioning that an entity instance cannot be tracked because another instance with the same key value is already being tracked.DbUpdateConcurrencyExceptionafterSaveChanges(), usually because a concurrency token did not match any row.DbUpdateExceptionwrapping a provider-specific database error, such as a unique constraint or foreign-key violation.
Cause and diagnostic table
| Exception | Likely cause | Where to look |
|---|---|---|
InvalidOperationException about duplicate tracking | Same key attached or loaded twice in one context instance | ChangeTracker.Entries() before SaveChanges() |
DbUpdateConcurrencyException | Concurrency token mismatch between client and database row | Failed SQL in context logging; original and database values in EntityEntry |
DbUpdateException | Provider or database constraint error | Inner exception and provider error code |
Ordered checks
- Read the full exception message and inner exception. Do not stop at the top-level message. The inner exception often contains the provider-specific SQL state.
- Inspect the change tracker immediately before the failing call. In a debugger or temporary log, iterate
context.ChangeTracker.Entries()and printEntityStateand primary-key values for every entry. - Call
ChangeTracker.DetectChanges(). This synchronizes snapshot tracking with current property values and can surface conflicts that were hidden untilSaveChanges(). - Enable
DbContextlogging. Capture the exact command text and parameters EF Core attempts to send. This distinguishes a tracker error from a database error. - Check the installed EF Core major version. Tracking semantics and client evaluation defaults changed in EF Core 3.0 and later. Verify behavior against your package version before applying a fix written for another release.
Example: listing tracked entries and key values
Where to run: Inside the application’s data-access layer or an isolated unit test.
Permissions: No special operating-system permissions are required; the code runs with the application’s identity.
Placeholders: Replace <YourDbContext> with your context class.
Expected checks: The list contains two entries with the same primary-key value, often in states such as Added, Modified, or Unchanged.
Risks: Logging parameter values can expose secrets; redact connection strings before sharing logs.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
var context = new <YourDbContext>(new DbContextOptionsBuilder<<YourDbContext>>()
.UseSqlServer("")
.LogTo(Console.WriteLine, LogLevel.Information)
.Options);
foreach (var entry in context.ChangeTracker.Entries())
{
var key = entry.Metadata.FindPrimaryKey();
var values = key?.Properties.Select(p => entry.Property(p.Name).CurrentValue)
?? Enumerable.Empty();
Console.WriteLine($"{entry.Entity.GetType().Name} | {entry.State} | {string.Join(",", values)}");
}
Fixes tied to findings
Duplicate tracked instance
If the tracker already contains an entity with the same key, detach the stale entry or reload it before saving.
var staleEntry = context.Entry(staleEntity);
staleEntry.State = EntityState.Detached;
// or
staleEntry.Reload();
Where to run: Application code, immediately before SaveChanges().
Risks: Reload() discards current client values unless they are explicitly merged back into the entity. ChangeTracker.Clear() detaches every tracked entity, including modified ones that may never be persisted.
Concurrency token mismatch
If DbUpdateConcurrencyException is thrown, reload the conflicting entry and reapply intended values, then retry SaveChanges(). Review whether the entity uses a concurrency token such as a rowversion column.
var entry = context.Entry(conflictingEntity);
entry.Reload();
// Reapply intended property values here
context.SaveChanges();
Read-only queries
For queries that do not update data, use AsNoTracking() so the context does not attach instances that could later conflict with attached entities.
var entities = context.YourEntities.AsNoTracking().ToList();
Context lifetime
Scope DbContext per request or per logical unit of work. Reusing a long-lived context across unrelated operations increases the chance that stale entities remain tracked.
Escalation criteria
- The exception persists after clearing the tracker and detaching duplicates.
- The failure occurs during serialization or deserialization of entity graphs.
- The failure affects production transactions with possible data loss.
- Concurrency tokens are involved and the database schema or token strategy needs review.
In these cases, involve database administration and review concurrency tokens before applying further code changes.
Limitations and practical verification
SaveChanges() is not automatically retry-safe. Depending on the provider and transaction configuration, partial writes may have already occurred when an exception is thrown.
To verify a fix, reproduce the conflict in an isolated console or unit test using the in-memory provider: attach two entities with identical keys to one DbContext instance and call SaveChanges(). Inspect context.ChangeTracker.Entries() before the call, then confirm that the chosen fix resolves the exception without losing intended changes. Compare results against the installed EF Core package version, because tracking behavior can change between major releases.