Reading and Fixing a SQL Server Deadlock

🚨Part of the SQL Server Errors series, the exact messages and what actually causes them.

Msg 1205  ·  Level 13  ·  State 51
Transaction (Process ID 52) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
The engine picked the cheaper transaction to kill, and it was yours. The error is the symptom. The fix is in the order the two transactions touched their objects, which the deadlock graph in the system_health session will show you.

A deadlock is SQL Server refusing to let two sessions wait on each other forever. Session A holds a lock Session B needs, Session B holds a lock Session A needs, neither can proceed, so the deadlock monitor picks one as the victim, kills it with error 1205, and lets the other continue. That’s the engine working correctly. The actual problem is almost always in application logic or index design, not in SQL Server itself, and the deadlock graph tells you exactly where.

Get Deadlock Summary captures deadlock events from the system_health Extended Events session, no custom trace required, they’re captured by default on every SQL Server instance since 2008. This post is about reading what it gives you and turning that into an actual fix.


STEP1

Step 1: Confirm It’s Actually a Deadlock

Applications sometimes misreport a timeout or a long block as a “deadlock.” Confirm from the real error first:

Transaction (Process ID 63) was deadlocked on lock resources with another process
and has been chosen as the deadlock victim. Rerun the transaction.

Error 1205, specifically. A generic timeout (Msg -2, Timeout expired) or a long block that never actually deadlocked is a different problem, see Troubleshoot SQL Server Blocking for that case instead. They look similar to an end user (the query didn’t finish) but need different fixes.


STEP2

Step 2: Pull the Deadlock Graph

Run Get-DeadlockSummary to get recent deadlock events with their XML graphs. Every deadlock graph has the same two-part shape:

  • <process-list>, one entry per session involved, showing what each was executing, what lock mode it wanted, and which one was chosen as victim (marked in the graph)
  • <resource-list>, the actual objects and lock resources each process held and was waiting for, this is where you find the table/index names

The victim process isn’t necessarily the “cause.” SQL Server picks based on transaction cost (cheapest to roll back, by default) or DEADLOCK_PRIORITY if you’ve set it explicitly. Both sessions are equally part of the cycle; reading only the victim’s query and ignoring the survivor’s is the most common way to misdiagnose a deadlock.

Microsoft’s reference covers MSSQLSERVER_1205 and the Deadlocks guide in full.


STEP3

Step 3: Identify the Access Pattern

Read both processes’ queries and lock resources together and ask: what order is each one acquiring locks in? The classic deadlock shape is two sessions touching the same two resources in opposite order:

  • Session A: updates Table 1, then Table 2
  • Session B: updates Table 2, then Table 1

If A gets Table 1’s lock first and B gets Table 2’s lock first, both are now waiting on the other, and neither can win. This is by far the most common real-world cause, and it’s an application logic problem: whatever two operations these are, they need to touch resources in the same order every time.

The second most common shape is a single statement deadlocking against itself under concurrency, usually because of missing indexes forcing a full scan that takes locks across a much wider range than the query actually needs, or a lock escalation from row/page locks to a table lock while another session already holds a conflicting lock elsewhere on the same table.


Common Causes and Fixes

CauseFix
Inconsistent access order across transactionsThe only real fix is a consistent order everywhere the same tables are touched together. A code review finding, not a database setting, and the cause behind most deadlocks that survive every other change.
Missing indexes causing wide scansA query that should touch 5 rows and scans 50,000 takes far more locks than it needs, so it collides with far more sessions. Run Get Missing Indexes against the tables named in the deadlock graph.
Lock escalation to a table lockRoughly 5,000 locks on one object and a statement escalates. Two sessions that coexisted fine at row granularity deadlock the moment one of them escalates. Either ALTER TABLE ... SET (LOCK_ESCALATION = AUTO), which is partition aware, or break the batch into smaller chunks.
Transactions held open longer than the work needsAn update, then a call out to slow application code, then a commit, holds every lock for the whole round trip. Do the slow work before or after the transaction, never inside it.
READ COMMITTED without READ_COMMITTED_SNAPSHOTUnder the default isolation level readers take shared locks that can join a deadlock cycle with writers. Row versioning removes that entire category of reader/writer deadlock, at the cost of tempdb version-store overhead. Check Get Temp DB Hotspots before enabling it on a tempdb-constrained instance.

What Doesn’t Actually Fix a Deadlock

  • Retry logic alone. Catching error 1205 and retrying the transaction makes the application resilient to the symptom, and it’s a reasonable defensive practice, but it doesn’t reduce how often the deadlock happens. Pair it with an actual root-cause fix, don’t rely on it as the fix.
  • NOLOCK on the involved queries. This can reduce deadlocks involving that specific reader by skipping shared locks entirely, but it does so by allowing dirty reads, uncommitted data can be returned. That’s a data-correctness trade, not a deadlock fix, and it’s the wrong tool outside a small set of genuinely tolerant reporting queries.
  • Raising DEADLOCK_PRIORITY on one session. This changes which session becomes the victim, not whether a deadlock happens. Useful when one transaction is genuinely more important to protect, but it’s a mitigation for the symptom’s impact, not a fix for the cause.

Best Practices

  • Capture and review deadlock graphs as they happen rather than only after a user complains; system_health retains a rolling window, not forever.
  • When two tables are updated together anywhere in the application, audit every code path that touches both and confirm the access order matches.
  • Treat a recurring deadlock pattern (same tables, same shape, repeatedly) as a design problem worth fixing properly, not a “just add retry logic” situation.
  • Re-run Get Deadlock Summary after a fix to confirm the specific pattern actually stopped, not just that deadlocks feel less frequent.


Common Questions

Is a deadlock the same as blocking?
No, and confusing them wastes time. Blocking is one transaction waiting, and it resolves on its own when the other finishes. A deadlock is two transactions waiting on each other, which can never resolve, so SQL Server kills one. If you are seeing Msg 1222 instead, that is a lock timeout, which is blocking.
Can I stop my transaction being chosen as the victim?
SET DEADLOCK_PRIORITY HIGH makes another session more likely to be chosen, but it does not stop deadlocks happening. It moves the pain rather than fixing it, and is worth using only for a genuinely more important process.
Where do I find the deadlock graph?
The system_health extended events session captures deadlock graphs by default and is running on every modern instance. You do not need to set up a trace to catch the next one, the last several are usually already there.
Can I catch 1205 in TRY/CATCH and retry?
Yes. Unlike a compile-time error, 1205 is raised while the batch runs, so a CATCH block sees it and can re-run the whole transaction, which the victim has already had rolled back. Bound the retries and add a short delay, and treat the retry as a way of surviving the symptom, not as the fix: the access-order problem above is still there.


Related Scripts

Comments

Leave a Reply

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