DOTNET Contents

Transactions & Isolation Levels

Transactions are where reliability goes to die: hidden ambient transactions, wrong isolation, and long locks. Learn how to pick isolation levels, diagnose blocking, and keep transactions short and safe.

On this page

Production incident

An endpoint creates an order and updates inventory. Under load you see oversells and negative inventory. A quick patch adds a big transaction around the entire request including HTTP calls to payment. Now you stop oversells but you introduce lock contention and a cascading outage.

Symptom

  • Data anomalies: double-spend, oversell, inconsistent reads.
  • After a patch: timeouts, blocking chains, deadlocks.
  • DB CPU is moderate but wait time is high.

Cause

  • Using default isolation without understanding semantics.
  • Long-running transactions that include remote calls or large loops.
  • Mixing EF Core SaveChanges calls across multiple aggregates without an explicit boundary.

Diagnosis

# App side: identify slow requests with DB spans or logs
grep -R "BeginTransaction" -n /var/log/app/
grep -R "deadlock" -n /var/log/app/

# When you can run DB inspection, look for blocking and deadlocks
# SQL Server deadlock graphs are best pulled from extended events / system_health

Anti-pattern

  • Wrapping an entire HTTP request in a transaction that includes outbound IO.
  • Relying on implicit transaction behavior and assuming it is enough.
  • Escalating isolation to Serializable as a default fix.

Correct pattern

Keep transactions short, cover only DB work, and choose isolation per scenario.

// Short DB-only transaction boundary
await using var tx = await db.Database.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted);

try
{
    // Make updates deterministic and indexed
    // Example: conditional update to prevent negative inventory
    var affected = await db.Database.ExecuteSqlInterpolatedAsync($@"
        UPDATE Inventory
        SET Quantity = Quantity - {qty}
        WHERE Sku = {sku} AND Quantity >= {qty}
    ");

    if (affected == 0) throw new InvalidOperationException("Insufficient inventory");

    db.Orders.Add(order);
    await db.SaveChangesAsync();

    await tx.CommitAsync();
}
catch
{
    await tx.RollbackAsync();
    throw;
}

Isolation level guidance

  • ReadCommitted: baseline. Most OLTP. Still can block on writers.
  • Snapshot / RCSI: reduces reader-writer blocking. Requires DB config and tempdb capacity planning.
  • RepeatableRead / Serializable: use sparingly for narrow critical sections, with proper indexes, and only when you accept lock cost.

Security and performance impact

  • Performance: long transactions amplify lock duration and increase deadlock risk.
  • Security: inconsistent reads can leak business invariants (for example showing a resource as available when it is not). Also, deadlock storms can become an availability incident.

Operational notes

  • Monitoring: track deadlocks, lock waits, transaction duration, and timeouts.
  • Rollout: introduce transaction changes behind a feature flag for high-traffic endpoints.
  • Rollback: be ready to revert isolation or remove long transactions quickly. Prefer forward-fix with narrower SQL updates.

Checklist

  • Transactions wrap only DB work, no remote calls inside.
  • Transaction duration is bounded and measurable.
  • Isolation level is chosen intentionally per endpoint.
  • Updates use indexed predicates to avoid wide locks.
  • Deadlocks and lock waits are monitored and alerted.