A SQL Server application can work perfectly during development and still fail with deadlock errors when multiple users access the same data at the same time. One transaction updates an order, another updates its inventory record, and both need resources held by the other transaction. Neither can continue.
SQL Server detects this situation and chooses one transaction as the deadlock victim. The victim receives error 1205, while SQL Server rolls back its transaction so the other transaction can proceed.
A common response is to catch the exception and retry the operation. Retrying can help, but doing it without understanding the deadlock may only hide the underlying problem. It can also increase database load or cause duplicate side effects if the application is not designed carefully.
In this article, we will look at why SQL Server deadlocks happen, how to investigate them, and how to handle them safely in application code.
What Is a SQL Server Deadlock?
A deadlock occurs when two or more transactions hold resources that the others need, creating a circular wait.
Consider two transactions that update the same two tables in a different order.
Transaction A updates the Orders table first and then needs the Inventory table. Transaction B updates Inventory first and then needs Orders.
<CodeBlock language="mermaid" editable={false}>flowchart TD
A["Transaction A holds Orders lock"] --> B["Transaction A waits for Inventory"]
C["Transaction B holds Inventory lock"] --> D["Transaction B waits for Orders"]
B --> E["Circular wait: deadlock"]
D --> E
E --> F["SQL Server selects a victim"]
F --> G["Victim transaction is rolled back"]
F --> H["Other transaction continues"]</CodeBlock>
Neither transaction can proceed while the other holds the required lock. SQL Server detects the cycle and terminates one transaction.
This is different from a normal blocking situation. Blocking occurs when one transaction waits for another to release a resource. A deadlock requires a circular dependency that prevents the involved transactions from making progress.
Deadlocks are not necessarily evidence of a SQL Server bug. They can occur in correctly functioning applications when concurrent transactions acquire locks in conflicting patterns.
A Simple Example of How Deadlocks Happen
Suppose an application transfers stock between two warehouses. Each warehouse has a row in an Inventory table.
One transaction locks warehouse A and then attempts to lock warehouse B. Another transaction does the opposite.
-- Transaction A
BEGIN TRANSACTION;
UPDATE dbo.Inventory
SET Quantity = Quantity - 1
WHERE WarehouseId = 1;
WAITFOR DELAY '00:00:05';
UPDATE dbo.Inventory
SET Quantity = Quantity + 1
WHERE WarehouseId = 2;
COMMIT TRANSACTION;
A second transaction might perform the updates in reverse order:
-- Transaction B
BEGIN TRANSACTION;
UPDATE dbo.Inventory
SET Quantity = Quantity - 1
WHERE WarehouseId = 2;
WAITFOR DELAY '00:00:05';
UPDATE dbo.Inventory
SET Quantity = Quantity + 1
WHERE WarehouseId = 1;
COMMIT TRANSACTION;
The WAITFOR statement makes the timing easier to demonstrate. It should not be added to production code.
If the transactions overlap, each can hold a lock on one row while waiting for the other row. SQL Server may detect the deadlock and roll back one transaction.
The same pattern can occur in real applications when different code paths update related records in different orders.
How to Find the Cause of a Deadlock
Before changing application code, find out which resources were involved and why the transactions needed them.
1. Capture the deadlock graph
SQL Server provides deadlock information through Extended Events. The built-in system_health session commonly captures xml_deadlock_report events, making it a useful starting point.
You can inspect the session's event file with a query like this:
SELECT
XEvent.value(
'(event/@timestamp)[1]',
'datetime2'
) AS DeadlockTime,
XEvent.query(
'(event/data/value/deadlock)[1]'
) AS DeadlockGraph
FROM
(
SELECT CAST(event_data AS XML) AS XEvent
FROM sys.fn_xe_file_target_read_file(
'system_health*.xel',
NULL,
NULL,
NULL
)
WHERE object_name = 'xml_deadlock_report'
) AS Deadlocks
ORDER BY DeadlockTime DESC;
The exact file location and permissions depend on your SQL Server installation. On some systems, the wildcard path may need to be replaced with the actual Extended Events file path.
The deadlock graph shows the participating processes, locked resources, requested locks, and the transaction SQL involved. Use it to identify the actual conflict instead of guessing from the application exception alone.
2. Identify the victim and participating statements
In the deadlock graph, look for:
- The victim process selected by SQL Server.
- The statements executed by each participating transaction.
- The tables, indexes, keys, or other resources involved.
- The lock modes held and requested.
- The order in which each transaction accessed those resources.
For example, if two transactions consistently update the same tables in opposite orders, standardizing the access order may remove the circular wait.
If the graph shows scans or a large number of locked rows, investigate whether the query needs a better index or a more selective predicate.
3. Check whether the problem is recurring
One deadlock during an unusual workload may require a different response from hundreds of deadlocks every minute.
Track the frequency of error 1205, the affected operations, transaction duration, and the impact on users. Compare deadlock occurrences with deployment changes, scheduled jobs, and traffic spikes.
This information helps prioritize the fixes that will produce the largest improvement.
Reduce Deadlocks Before Adding Retries
The best deadlock strategy is to reduce the likelihood of conflicting transactions.
Keep transactions short
Long-running transactions hold locks for longer periods and give other transactions more opportunities to conflict.
Avoid unnecessary network calls, user interaction, or lengthy calculations while a database transaction is open.
Perform independent work before starting the transaction when possible, and commit as soon as the required database changes are complete.
Access resources in a consistent order
If several operations update the same set of tables, make them acquire resources in a consistent order.
For example, if one operation updates Orders before Inventory, another operation that touches both should follow the same logical order where possible.
Consistent ordering reduces circular waits, although it does not eliminate every possible deadlock.
Review indexes and query plans
Poorly indexed queries can scan more rows than necessary. Depending on the query plan, isolation level, and access pattern, this can increase locking and contention.
Review the execution plan and verify that indexes support the actual predicates and joins. Do not add indexes blindly: every additional index also increases the cost of data modifications.
Use an appropriate isolation level
Isolation levels influence concurrency behavior, but changing them without understanding the application can introduce different consistency problems.
For example, enabling READ_COMMITTED_SNAPSHOT can reduce certain reader-writer blocking patterns by using row versions. It does not eliminate all deadlocks, especially those involving writers.
Choose an isolation strategy based on the application's consistency requirements and test its effects under realistic concurrent workloads.
Join the conversation! Your thoughts help the community grow.