Tips to avoid deadlocks in Entity Framework applications

UPDATE: Andres Aguiar has published some additional tips on his blog that you may also find helpful when dealing with deadlocks.

Recently a customer asked a question about how to avoid deadlocks when using EF. Let me first say very clearly that I don’t actually hear about deadlock issues with EF often at all. But deadlocks are a general problem with database applications using transactions, and to answer this particular customer I collected some information that then I thought could be worth sharing in my blog in case someone else runs into problems:

I won’t spend time in the basics of deadlocks or transaction isolation levels. There is plenty of information out there. In particular, here is a page in SQL Server’s documentation that provides general guidelines on how to deal with deadlocks in database applications. Two of the main recommendations that apply to EF applications are to examine the isolation level used in transactions and the ordering of operations.

Transaction isolation level

Entity Framework never implicitly introduces transactions on queries. It only introduces a local transaction on SaveChanges (unless an ambient System.Transactions.Transaction is detected, in which case the ambient transaction is used).

The default isolation level of SQL Server is actually READ COMMITTED, and by default READ COMMITED uses shared locks on reads which can potentially cause lock contention, although locks are released when each statement is completed. It is possible to configure a SQL Server database to avoid locking on reads altogether even in READ COMMITTED isolation level by setting the READ_COMMITTED_SNAPSHOT option to ON. With this option, SQL Server resorts to row versioning and snapshots rather than shared locks to provide the same guarantees as the regular READ COMMITED isolation. There is more information about it in this page in the documentation of SQL Server.

Given SQL Server’s defaults and EF’s behavior, in most cases each individual EF query executes in its own auto-commit transaction and SaveChanges runs inside a local transaction with READ COMMITED isolation.

That said, EF was designed to work very well with System.Transactions. The default isolation level for System.Transactions.Transaction is Serializable. This means that if you use TransactionScope or CommitableTransaction your are by default opting into the most restrictive isolation level, and you can expect a lot of locking!

Fortunately, this default can be easily overriden. To configure the Snapshot, for instance, using TransactionScope you can do something like this:

 1: using (var scope = new TransactionScope(TransactionScopeOption.Required, new 
 2: TransactionOptions { IsolationLevel= IsolationLevel.Snapshot }))
 3: {
 4: // do something with EF here
 5: scope.Complete();
 6: }

My recommendation would be to encapsulate this constructor in a helper method to simplify its usage.

Ordering of operations

EF does not expose a way to control the ordering of operations during SaveChanges. EF v1 indeed had specific issues with high isolation levels (e.g. Serializable) which could produce deadlocks during SaveChanges. It is not a very well publicized feature, but in EF 4 we changed the update pipeline to use more deterministic ordering for uncorrelated CUD operations. This helps ensure that multiple instances of a program will use the same ordering when updating the same set of tables, which in turns helps reduce the possibility of a deadlock.

Besides SaveChanges, if you need to have transactions with high isolation while executing queries, you can manually implement a similar approach: make sure your application always accesses the same pair of tables in the same order, e.g. use alphabetical order.


The recommendations to avoid deadlocks in EF applications boils down to:

  • Use snapshot transaction isolation level (or snapshot read committed)
  • Use EF 4.0 or greater
  • Try to use the same ordering when querying for the same tables inside a transaction


Hope this helps,

Comments (10)

  1. Mukesh says:

    In the EF code, I have  TransactionScope  as ReadCommitted Isolation. I set the SNAPSHOT ISOLATION to ON. But READ COMMITTED SNAPSHOT is OFF. I am still facing locking issues. Any ideas?

  2. Hello Mukesh,

    As long as the TransactionScope is created with ReadCommitted and READ_COMMITTED_SNAPSHOT is not enabled in the database then you are not really following the recommendations I explained in this blog post and I assume that you might be seeing locking issues because you are in fact using the default implementation of READ COMMITTED, which uses locking.

    While READ COMMITTED and SNAPSHOT are two different transaction isolation levels you can choose from by passing an argument to TransactionScope, READ_COMMITTED_SNAPSHOT setting on the other hand is a database-wide setting that replaces the default implementation of READ COMMITTED which uses locking with one that has READ COMMITTED semantics but uses snapshots.

  3. Preyash says:

    Hi Diego,

    Nice post. My question is that if I use "IsolationLevel.Snapshot" do I need to change SQL Server's default isolation level from READ COMMITTED to something else? If so then what?



  4. Santanu Kumar Raul says:

    Twitter : @raulsantanu In reference with my ask in twitter to you and thanks for replying me back .

    Hi Diego,

    We have a scenario where two or multiple partners can accept a lead (a record in a table) . Once one of them accepts nobody can see it in the "New" tab of the website . The logic behind acceptance of a lead are

    Step 1 – Check the nullability of AcceptedBy column of Lead table

    Step 2 – If it is not null then return

    Step 3 – else accept the lead (update AccptedBy in DB)

    Step 4 – Send mail to partner

    The problem we are facing when two partners are trying to accept a common a lead with very minute gap.

    First partner crosses first two steps and by the time it touches or has not touched third step ,

    second partner crosses Step-1 and comes to "else" part as DB commit has not been happened for 1st partner yet .

    So both the partner now able to accept the same lead and two mails which shakes the main business .

    fundamental of our application .

    We only could repro the scenario by following the below steps in dev env .

    1 – Delay (Thread.sleep (60000)) of 60 second after nullability check and before acceptance of lead

    2 – Open two browser in two diff. machines by two diff. partners

    3 – Both the partner choose a common lead

    4 – First partner hits the Accept button and after 10 second second partner hits the Accept button

    We are using code first EF .

    The codes are as below :

    public override AcceptLeadResponse Execute(AcceptLeadRequest request)


               if (request == null || request.LeadIds == null)

                   throw new ArgumentNullException("request");

               var response = new AcceptLeadResponse();

               foreach (var id in request.LeadIds)


                   var isLeadAlreadyAccepted = _leadRepository.SelectWithAcceptedBy(id).AcceptedBy == null ? false : true;

                   if (isLeadAlreadyAccepted)


                       response.Status = AcceptLeadResponseStatusCodes.SelectedLeadsAlreadyAccepted;

                       return response;



    ————-  Some other steps ——-

               if (openLeadCount + request.LeadIds.Count() <= partner.OpenLeadAllotment)


                   foreach (var leadId in request.LeadIds)


                       var lead = _leadRepository.Select(leadId);



                       _leadRepository.Accept(leadId, partner);

                      ——-Send Mail———-


                   response.Status = AcceptLeadResponseStatusCodes.LeadsAccepted;




                   response.Status = AcceptLeadResponseStatusCodes.OpenLeadAllotmentExceeded;


               return response;




    How to mitigate the issue ?

    We only could by doing another check before lead acceptance but I am sure this is gonna fail once load is more .

  5. Mukesh Vaishnav says:

    Hi Diego,

    Thank You very much such a nice and helpful post.

  6. amir says:

    my dbcontext is per request can i still use TransactionScope right where i need to make a select statement and not affecting other operations?

  7. @amir: I can't think of a reason this wouldn't work. Keep in mind that your SELECT query will get executed when you do something to iterate over its results.

    1. Sergey says:

      Hello Diego,

      could you, please, clarify this: if I set explicitly READ_COMMITTED with a scope and in another scope set READ_UNCOMMITED, does the first scope (READ_COMMITED) influence on the second one (READ_UNCOMMITTED) and restricts it as READ_COMMITTED too? Or each scope is independent of any other scopes?

  8. Rune G says:

    I just want to warn you about behaviour of the database connection and connection pooling together with transaction scope and setting isolation level on this.

    If you work with SQL Server 2012, the last set isolation level will follow the connection all the time, even when retrieved back from the connection pool by a different context. This will for example do that if you set a transaction scope to read uncommitted the connection will do dirty reads all the time until next change of transaction scope

    On SQL Server 2014, the isolation level set on the transaction scope is only valid for the first command done within the scope, the rest of the commands will be executed on the default setting of the database.

    As for SQL Server 2014; instead of using transaction scopes it is better to use ordinary db transaction context (context.Database.BeginTransaction()) instead, this seems to honor the isolation level throughout the transaction and resetting it on disposing of the transaction. The limitation of this is of course the distributed transaction logic handled by transaction scopes. This does however not work in SQL Server 2012.

    I have written a longer description of my findings on stack overflow:…/entity-framework-and-transactionscope-doesnt-revert-the-isolation-level-after-d

    1. Sergey says:

      Hello Rune,
      thank you for sharing this information!
      >On SQL Server 2014, the isolation level set on the transaction scope is only valid for the first command done within the scope, the rest >of the commands will be executed on the default setting of the database.
      I checked it and it really doesn’t work sometimes even for the 1-st command within the scope: if “sp_reset_connection” goes then on “Audit Loing” “set transaction isolation level read committed” goes. I am very surprised that (TransactionScope scope ..) doesn’t work as expected and there are no updates since then.

Skip to main content