Chapter 07 · Transactions, Locking, Row Versioning, Isolation, and Deadlocks

Deadlock Graphs, Blocking Chains, Timeouts, Retry Policies, and Contention Testing

Separate blocking, timeout, and deadlock; capture xml_deadlock_report evidence; repair cycles; and use bounded retries only for replay-safe operations.

Advanced135–175 minutesDeadlock + Extended Events labSQL Server 2025 CU7 · Developer/Expresssystem_health + two local sessionsLast reviewed: August 2026

Learning outcomes

Two ServiceHub workers update the same two inventory rows in opposite order. Each holds one lock and waits for the other: a real deadlock. A different request merely waits behind a blocker; another stops waiting because LOCK_TIMEOUT expires. These outcomes have different evidence and different remediation.

01

Distinguish blocking, lock timeout, and deadlock victim errors.

02

Create a safe two-session deadlock and capture xml_deadlock_report.

03

Interpret victim, process, and resource nodes.

04

Repair the underlying cycle before adding retry.

05

Apply bounded retry only to replay-safe/idempotent operations.

Failure injection

Run this only against disposable lab07 rows on a local Developer/Express instance. One transaction will be selected as a victim and rolled back.

1. Three timelines, three diagnoses

Blocking is a wait where the blocker can still progress. A lock timeout happens when the waiting session exceeds its configured LOCK_TIMEOUT, commonly producing error 1222. A deadlock is a dependency cycle with no natural progress path; SQL Server chooses a victim and returns error 1205. A timeout is not evidence that a deadlock occurred.

sql · bounded timeout experiment
-- Session ABEGIN TRANSACTION;UPDATE lab07.Inventory SET on_hand=on_hand WHERE part_id=10;-- keep open briefly-- Session B-- SET LOCK_TIMEOUT 3000;-- SELECT * FROM lab07.Inventory WHERE part_id=10;-- reset afterward: SET LOCK_TIMEOUT -1;

2. Create the classic opposite-order cycle

sql · Session A — 10 then 20
BEGIN TRY  BEGIN TRANSACTION;  UPDATE lab07.Inventory SET on_hand=on_hand WHERE part_id=10;  WAITFOR DELAY '00:00:03';  UPDATE lab07.Inventory SET on_hand=on_hand WHERE part_id=20;  COMMIT;END TRYBEGIN CATCH  SELECT ERROR_NUMBER() AS error_number,ERROR_MESSAGE() AS error_message,XACT_STATE() AS xact_state;  IF XACT_STATE()<>0 ROLLBACK;END CATCH;GO
sql · Session B — 20 then 10
BEGIN TRY  BEGIN TRANSACTION;  UPDATE lab07.Inventory SET on_hand=on_hand WHERE part_id=20;  WAITFOR DELAY '00:00:03';  UPDATE lab07.Inventory SET on_hand=on_hand WHERE part_id=10;  COMMIT;END TRYBEGIN CATCH  SELECT ERROR_NUMBER() AS error_number,ERROR_MESSAGE() AS error_message,XACT_STATE() AS xact_state;  IF XACT_STATE()<>0 ROLLBACK;END CATCH;GO

Start both batches close together. One should receive 1205. Do not promise which session loses: victim choice can depend on deadlock priority and estimated rollback cost.

3. system_health already captures the recommended event

Microsoft recommends the xml_deadlock_report Extended Event. The default system_health session captures it, so an operator often has deadlock graphs without enabling SQL Trace/Profiler or creating a broad custom session. A deadlock graph contains a victim list, process list, and resource list that reconstruct the cycle.

sql · read deadlock XML from the system_health ring buffer
WITH x AS(  SELECT CAST(t.target_data AS xml) AS target_data  FROM sys.dm_xe_session_targets AS t  JOIN sys.dm_xe_sessions AS s ON s.address=t.event_session_address  WHERE s.name=N'system_health' AND t.target_name=N'ring_buffer')SELECT n.event_data.query('.') AS deadlock_eventFROM xCROSS APPLY target_data.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS n(event_data);GO

Ring-buffer history is finite. Export incident evidence promptly; deadlock XML can include SQL text, object names, host/application details, and therefore sensitive operational information.

4. Repair resource order before adding retry

The direct repair for this lab is consistent ordering: both transactions update part 10 before part 20. Real graphs may instead point to broad scans, missing indexes, long transactions, inconsistent object order, lookup/update interactions, or inappropriate isolation. Repair must follow the actual graph.

sql · consistent resource order
BEGIN TRANSACTION;UPDATE lab07.Inventory SET on_hand=on_hand WHERE part_id=10;UPDATE lab07.Inventory SET on_hand=on_hand WHERE part_id=20;COMMIT;GO
Wrong approach

Catching 1205 and retrying forever can amplify load and duplicate nontransactional effects. Retry is a resilience layer after recurring deadlocks are diagnosed and the operation is known to be replay-safe.

5. Retry is an application correctness policy

A deadlock victim transaction is rolled back by SQL Server. A bounded retry can be reasonable for error 1205 when the complete operation is idempotent or otherwise safe to replay. Use backoff/jitter, an overall deadline, cancellation, and correlation telemetry. If the operation charges a card, sends email, or calls an external service outside the database atomic boundary, retry needs an idempotency/outbox design.

text · bounded retry pseudocode
for attempt in 1..max_attempts:    try:        begin transaction        execute replay_safe_unit_of_work        commit        break    catch sql_error 1205:        rollback if needed        emit telemetry(correlation_id, attempt)        if attempt == max_attempts: raise        sleep(jittered_backoff(attempt))    catch other_error:        rollback if needed        raise

Do not automatically treat lock-timeout error 1222 as identical to deadlock 1205. Timeout can be a service deadline or a chronic blocking symptom that immediate retry worsens.

6. Cleanup and bridge to storage internals

sql · drop disposable Chapter 07 objects
USE ServiceHubLab;GOIF @@TRANCOUNT>0 ROLLBACK;DROP TABLE IF EXISTS lab07.WorkAudit;DROP TABLE IF EXISTS lab07.Inventory;GOIF SCHEMA_ID(N'lab07') IS NOT NULLAND NOT EXISTS(SELECT 1 FROM sys.objects WHERE schema_id=SCHEMA_ID(N'lab07'))  EXEC(N'DROP SCHEMA lab07;');GO

Chapter 07 established a concurrency evidence model: transaction state, lock waits, isolation/versioning, version retention, and deadlock graphs. Chapter 08 descends into pages, extents, heaps, B-trees, and the transaction log—the storage structures behind durable changes and recovery.

Check your understanding

  1. How is timeout different from deadlock?
  2. Which Extended Event should capture deadlocks?
  3. Does system_health normally include it?
  4. What repairs the two-row lab?
  5. When is 1205 retry unsafe?
Review the answers

A timeout is one waiter giving up; a deadlock is a dependency cycle requiring victim selection.

xml_deadlock_report.

Yes.

Acquire the shared resources in a consistent order.

When replay can duplicate side effects or the operation is not otherwise retry-safe.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.