Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
SQL

Fix Deadlocks by Understanding Lock Ordering and Using Proper Isolation Levels

- 01.02.26 | 01.02.26 - ErcanOPAK

Getting “Transaction was deadlocked” errors randomly? Deadlocks happen when two transactions lock resources in opposite order. Here’s how to fix them permanently.

Identify Deadlocks – System Health Session:

-- SQL Server automatically logs deadlocks to system_health session
SELECT 
    CAST(target_data AS XML) AS deadlock_xml
FROM sys.dm_xe_session_targets st
JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
WHERE s.name = 'system_health'
    AND st.target_name = 'ring_buffer';

-- Extract deadlock details
WITH DeadlockData AS (
    SELECT CAST(target_data AS XML) AS TargetData
    FROM sys.dm_xe_session_targets st
    JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
    WHERE s.name = 'system_health'
)
SELECT 
    DeadlockGraph.query('.') AS DeadlockGraph,
    DeadlockGraph.value('(//deadlock/process-list/process/@lasttranstarted)[1]', 'datetime') AS LastTranStarted
FROM DeadlockData
CROSS APPLY TargetData.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS Deadlock(DeadlockGraph)
ORDER BY LastTranStarted DESC;

Common Deadlock Pattern:

-- Transaction 1:
BEGIN TRANSACTION
UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 123;  -- Locks Order 123
-- ... some processing ...
UPDATE OrderItems SET Shipped = 1 WHERE OrderId = 123;     -- Locks OrderItems for Order 123
COMMIT;

-- Transaction 2 (running simultaneously):
BEGIN TRANSACTION  
UPDATE OrderItems SET Reserved = 1 WHERE OrderId = 123;    -- Locks OrderItems for Order 123
-- ... some processing ...
UPDATE Orders SET Status = 'Reserved' WHERE OrderId = 123; -- Needs Order 123 lock → DEADLOCK!
COMMIT;

-- Result: One transaction wins, other gets "Transaction was deadlocked" error

Solution 1 – Consistent Lock Ordering:

-- Always access tables in the same order
-- Transaction 1:
BEGIN TRANSACTION
UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 123;       -- Order first
UPDATE OrderItems SET Shipped = 1 WHERE OrderId = 123;          -- OrderItems second
COMMIT;

-- Transaction 2:
BEGIN TRANSACTION  
UPDATE Orders SET Status = 'Reserved' WHERE OrderId = 123;      -- Order first (same order!)
UPDATE OrderItems SET Reserved = 1 WHERE OrderId = 123;         -- OrderItems second
COMMIT;

-- No deadlock: Both transactions wait for each other in the same sequence

Solution 2 – Use UPDLOCK Hint:

BEGIN TRANSACTION

-- Acquire update lock immediately (before SELECT)
SELECT @CurrentStatus = Status 
FROM Orders WITH (UPDLOCK, ROWLOCK) 
WHERE OrderId = 123;

-- Now safe to update (already have the lock)
IF @CurrentStatus = 'Pending'
BEGIN
    UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 123;
    UPDATE OrderItems SET Shipped = 1 WHERE OrderId = 123;
END

COMMIT;

Why UPDLOCK Works:
Normal SELECT takes shared lock → allows other SELECTs but blocks UPDATEs.
UPDLOCK takes update lock → blocks other updates AND other UPDLOCKs, preventing deadlock races.
When UPDATE executes, update lock converts to exclusive lock (no waiting, already owned).

Solution 3 – Reduce Transaction Scope:

-- BAD: Long transaction holding locks
BEGIN TRANSACTION
UPDATE Orders SET Status = 'Processing' WHERE OrderId = 123;
-- 5 seconds of business logic here...
-- Complex calculations...
-- External API calls...
UPDATE Orders SET Status = 'Complete' WHERE OrderId = 123;
COMMIT;
-- Locks held for 5+ seconds = high deadlock risk

-- GOOD: Minimize lock time
-- Do calculations outside transaction
var result = PerformComplexCalculation();
var apiResponse = await CallExternalAPI();

-- Now quick transaction
BEGIN TRANSACTION
UPDATE Orders SET Status = 'Complete', Result = @result WHERE OrderId = 123;
COMMIT;
-- Locks held for 0.01 seconds = minimal deadlock risk

Solution 4 – Use Snapshot Isolation:

-- Enable snapshot isolation (database level)
ALTER DATABASE YourDatabase 
SET ALLOW_SNAPSHOT_ISOLATION ON;

ALTER DATABASE YourDatabase 
SET READ_COMMITTED_SNAPSHOT ON;

-- Now SELECTs don't block UPDATEs and vice versa
-- Each transaction sees a consistent snapshot
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION
    SELECT * FROM Orders WHERE OrderId = 123;  -- Reads don't block writes
    -- ... processing ...
    UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 123;
COMMIT;

Handling Deadlocks in Application Code (C#):

public async Task UpdateOrderWithRetry(int orderId, int maxRetries = 3)
{
    for (int attempt = 0; attempt < maxRetries; attempt++) { try { using (var transaction = await _dbContext.Database.BeginTransactionAsync()) { var order = await _dbContext.Orders .FirstOrDefaultAsync(o => o.OrderId == orderId);
                    
                order.Status = "Shipped";
                
                await _dbContext.SaveChangesAsync();
                await transaction.CommitAsync();
                
                return true;
            }
        }
        catch (SqlException ex) when (ex.Number == 1205) // Deadlock error code
        {
            _logger.LogWarning($"Deadlock detected, attempt {attempt + 1}/{maxRetries}");
            
            if (attempt == maxRetries - 1)
                throw; // Give up after max retries
                
            // Exponential backoff
            await Task.Delay((int)Math.Pow(2, attempt) * 100);
        }
    }
    
    return false;
}

Related posts:

Get the row number "x" from SQL

How to make pagination in MS SQL Server

Database stuck in “Restoring” state

Post Views: 3

Post navigation

SQL Server: Find Slow Queries with Query Store (No Third-Party Tools Needed)
C# Performance: Use Span and Memory to Eliminate Array Allocations

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

February 2026
M T W T F S S
 1
2345678
9101112131415
16171819202122
232425262728  
« Jan    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (934)
  • How to add default value for Entity Framework migrations for DateTime and Bool (830)
  • Get the First and Last Word from a String or Sentence in SQL (822)
  • How to select distinct rows in a datatable in C# (799)
  • How to make theater mode the default for Youtube (708)
  • Add Constraint to SQL Table to ensure email contains @ (571)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (552)
  • Average of all values in a column that are not zero in SQL (517)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (473)
  • Find numbers with more than two decimal places in SQL (436)

Recent Posts

  • Windows 11 Snap Layouts: Organize 10+ Windows Without Alt+Tab Hell
  • Kubernetes Ingress: Expose Multiple Services Through One Load Balancer
  • Docker Compose: Launch Full Stack Apps with One Command (Node + Redis + Postgres)
  • Visual Studio Live Share: Real-time Collaborative Coding Like Google Docs
  • WordPress REST API: Turn Your Site into a Headless CMS for React/Vue Apps
  • WordPress Custom Post Types: Build Real Estate, Job Board, or Product Catalogs
  • Photoshop Batch Processing: Edit 1000 Images While You Sleep
  • Photoshop Smart Objects: Edit Once, Update Everywhere Without Quality Loss
  • Windows 11 WSL2: Run Linux at Native Speed Without Dual Boot
  • WordPress Speed Hack: How Lazy Loading Images Cuts Page Load Time in Half

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (934)
  • How to add default value for Entity Framework migrations for DateTime and Bool (830)
  • Get the First and Last Word from a String or Sentence in SQL (822)
  • How to select distinct rows in a datatable in C# (799)
  • How to make theater mode the default for Youtube (708)

Recent Posts

  • Windows 11 Snap Layouts: Organize 10+ Windows Without Alt+Tab Hell
  • Kubernetes Ingress: Expose Multiple Services Through One Load Balancer
  • Docker Compose: Launch Full Stack Apps with One Command (Node + Redis + Postgres)
  • Visual Studio Live Share: Real-time Collaborative Coding Like Google Docs
  • WordPress REST API: Turn Your Site into a Headless CMS for React/Vue Apps

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com