There's a moment when a transaction fails halfway through, and you realize rolling back everything is overkill. You did five inserts, the sixth one violates a constraint, and you'd rather undo just that last operation than start the whole transaction over.
That's what savepoints are for, and EF Core has supported them for a while now but until recently I didn't know this feature existed.
What's the problem with plain transactions?
A regular database transaction is all-or-nothing. Something like this:
using var transaction = await context.Database.BeginTransactionAsync();
try
{
context.Blogs.Add(new Blog { Url = "https://blog1.com" });
await context.SaveChangesAsync();
context.Blogs.Add(new Blog { Url = "https://blog2.com" });
await context.SaveChangesAsync();
// this one fails
context.Blogs.Add(new Blog { Url = null });
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
}
If the third SaveChangesAsync fails, the whole transaction gets rolled back. Blog1 and Blog2 are gone too, even though nothing was wrong with them. In a long-running transaction with many steps, that's expensive and often unnecessary.
Savepoints: rolling back to a specific point
A savepoint lets you mark a point inside a transaction, and later roll back to that point instead of the beginning. EF Core exposes this through CreateSavepoint, RollbackToSavepoint, and ReleaseSavepoint on the transaction object.
using var transaction = await context.Database.BeginTransactionAsync();
context.Blogs.Add(new Blog { Url = "https://blog1.com" });
await context.SaveChangesAsync();
await transaction.CreateSavepointAsync("BeforeSecondBlog");
context.Blogs.Add(new Blog { Url = "https://blog2.com" });
await context.SaveChangesAsync();
try
{
context.Blogs.Add(new Blog { Url = null });
await context.SaveChangesAsync();
}
catch
{
await transaction.RollbackToSavepointAsync("BeforeSecondBlog");
}
await transaction.CommitAsync();
Blog1 stays committed as part of the eventual commit, blog2's failed insert gets undone, and you can keep going from there — add a corrected blog2, or just move on. The rest of the transaction survives.
Remark: RollbackToSavepointAsync doesn't end the transaction. It puts you back at the state the savepoint captured, and you can continue issuing commands, create more savepoints, or commit normally afterwards.
Where this actually helps
Savepoints make the most sense in a few recurring scenarios:
- Batch processing — you're inserting a large number of related records in one transaction, and one bad record shouldn't cost you the whole batch.
- Optional steps — a step that's "nice to have" but not required for the transaction to make sense. Try it, savepoint before it, roll back if it fails, keep going.
- Retry logic within a transaction — instead of catching an exception and restarting the entire transaction, you retry from a savepoint closer to the failure.
Remark: Not every database provider supports savepoints the same way. SQL Server and PostgreSQL both support them through EF Core; check your provider's docs if you're on something less common. Also, SaveChangesAsync itself wraps its own operations in an implicit transaction unless you're already inside one. Savepoints only make sense once you're explicitly managing a transaction yourself.
Automatic savepoints
There's a detail worth knowing: EF Core creates savepoints automatically in some cases. When you call SaveChangesAsync multiple times within a user-initiated transaction, EF Core will insert a savepoint before each call so that if that particular SaveChangesAsync fails, it can roll back just that batch of changes and leave your transaction usable rather than aborting it outright. You get some of this behavior for free, without calling CreateSavepointAsync yourself.
That's it! A small feature, but a useful one to have in your back pocket for anything that touches multi-step transactions.