SQL Server: How to Find the Lead Blocker

SQL Server: How to Find the Lead Blocker — DMV Queries and Blocking Chain Analysis – SQLYARD

SQL Server: How to Find the Lead Blocker — DMV Queries and Blocking Chain Analysis


Compatibility: Query 1 and Query 3 are compatible with SQL Server 2005 and later. Query 2 (recursive CTE) requires SQL Server 2012 or later — it uses sys.dm_exec_sql_text() which replaced the deprecated ::fn_get_sql() removed in SQL Server 2012. All queries work on SQL Server 2012 through 2025.

Blocking is one of the most disruptive and misdiagnosed SQL Server performance problems. When users report slow queries or application timeouts during what appears to be low activity, blocking is often the cause — not CPU or I/O pressure.

The challenge is that blocking chains can involve many sessions. Session 55 is blocked by session 42, which is blocked by session 38, which is blocked by session 17. Session 17 is the one causing the problem. That is the lead blocker. Kill the wrong session and nothing changes.

Finding the lead blocker means finding the root session — the one holding locks that no other session is waiting on. Every blocked session in the chain traces back to it. These queries make that chain visible immediately.

What Is a Lead Blocker?

In SQL Server, a lead blocker (also called a root blocker or head blocker) is a session that:

  • Holds a lock on a resource
  • Is not itself blocked by any other session
  • Has one or more sessions waiting on its locks

In sys.dm_exec_requests and sysprocesses, the lead blocker is the session where blocking_session_id = 0 (not blocked by anyone), but other sessions show this session’s ID as their blocker.

Common causes of lead blockers include long-running explicit transactions left open, uncommitted transactions in application code, missing indexes causing large lock escalations, and batch operations that hold locks longer than expected.

Understanding the Blocking Chain

A blocking chain can be shallow (one blocker, one blocked session) or deep (many layers). The recursive CTE query in this article walks the entire chain regardless of depth.

Example Blocking Chain
SPID 17
Lead Blocker
blocking_session_id = 0
SPID 38
Blocked by 17
level 1
SPID 42
Blocked by 38
level 2
SPID 55
Blocked by 42
level 3
Kill SPID 17 → entire chain resolves immediately.
Kill SPID 38 → SPID 55 and 42 resolve, but 17 continues blocking other sessions.

Query 1 — Instant Snapshot with Lock and Object Details Recommended

This query uses modern DMVs to show active blocking with the requesting and blocking session query text, the locked object, lock type, and request mode — all in one result set. Best for live incident diagnosis.

-- Lead Blocker: Instant snapshot with lock details and both query texts
SELECT
    db.name                              AS DBName,
    tl.request_session_id                AS blocked_spid,
    wt.blocking_session_id               AS blocking_spid,
    OBJECT_NAME(p.OBJECT_ID)             AS BlockedObjectName,
    tl.resource_type,
    tl.request_mode,
    h1.TEXT                              AS RequestingText,
    h2.TEXT                              AS BlockingText
FROM sys.dm_tran_locks                   AS tl
INNER JOIN sys.databases                 db
    ON db.database_id = tl.resource_database_id
INNER JOIN sys.dm_os_waiting_tasks       AS wt
    ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.partitions                AS p
    ON p.hobt_id = tl.resource_associated_entity_id
INNER JOIN sys.dm_exec_connections       ec1
    ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections       ec2
    ON ec2.session_id = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2;

Columns Returned

ColumnWhat It Tells You
DBNameWhich database the lock is in
blocked_spidThe session waiting for the lock
blocking_spidThe session holding the lock — this is your lead blocker
BlockedObjectNameThe table or object being locked
resource_typeType of resource locked (OBJECT, PAGE, KEY, RID)
request_modeLock mode being requested (S, X, U, IS, IX)
RequestingTextFull query text of the blocked session
BlockingTextFull query text of the blocking session — what is holding the lock

This query joins on tl.resource_associated_entity_id = p.hobt_id — it will only return rows where the lock is at the row, page, or key level (HoBT-based). Table-level locks (resource_type = 'OBJECT') will not appear in this result. Use Query 3 to catch those.

Query 2 — Recursive Blocking Chain with Lead Blocker and Level Updated

This recursive CTE walks the entire blocking chain from the lead blocker down to the deepest blocked session, showing each session’s level in the chain and its query text. Ideal for deep chains involving many sessions.

Compatibility note: The original version of this query used ::fn_get_sql() which was deprecated in SQL Server 2012 and removed in later versions. The query below uses sys.dm_exec_sql_text() — the correct modern replacement that works on SQL Server 2012 through 2025.

-- Recursive Lead Blocker Chain (modern version — works on SQL Server 2012+)
WITH LeadBlockers (blocker, blockee, level, sql_text) AS
(
    -- Anchor: sessions that are NOT blocked but ARE blocking others (lead blockers)
    SELECT
        s.blocked   AS blocker,
        s.spid      AS blockee,
        0           AS level,
        t.text      AS sql_text
    FROM master..sysprocesses s
    OUTER APPLY sys.dm_exec_sql_text(s.sql_handle) t
    WHERE s.blocked = 0
    AND EXISTS
    (
        SELECT 1 FROM master..sysprocesses inner_s
        WHERE inner_s.blocked = s.spid
    )

    UNION ALL

    -- Recursive: sessions blocked by sessions already in the CTE
    SELECT
        s.blocked   AS blocker,
        s.spid      AS blockee,
        lb.level + 1,
        t.text      AS sql_text
    FROM master..sysprocesses s
    INNER JOIN LeadBlockers lb ON s.blocked = lb.blockee
    OUTER APPLY sys.dm_exec_sql_text(s.sql_handle) t
)
SELECT
    blocker,
    blockee,
    level,
    LEFT(sql_text, 500) AS sql_text
FROM LeadBlockers
ORDER BY level, blocker;

How to Read the Results

ColumnWhat It Means
blockerSPID of the session causing the block. At level 0, this is 0 — meaning the lead blocker is blocked by nobody.
blockeeSPID of the session being blocked. At level 0, this is your lead blocker SPID.
levelDepth in the chain. Level 0 = lead blocker. Level 1 = directly blocked by lead blocker. Level 2 = blocked by a level-1 session, and so on.
sql_textThe query text currently executing or most recently executed in that session.

Reading example: If the results show blocker=0, blockee=17, level=0 — SPID 17 is your lead blocker. Any row showing blocker=17 at level 1 is directly blocked by it. Work from level 0 upward to understand the full chain.

Query 3 — Simple Blocking Detection with Wait Info

The simplest and fastest blocking check. Returns all sessions currently blocked, their blocker, wait type, wait time, and wait resource. No joins required — runs instantly even under heavy load.

-- Simple blocking detection: all currently blocked sessions
SELECT
    r.session_id                         AS blocked_spid,
    r.blocking_session_id                AS blocking_spid,
    r.wait_type,
    r.wait_time                          AS wait_time_ms,
    r.wait_resource,
    DB_NAME(r.database_id)               AS database_name,
    LEFT(t.text, 500)                    AS blocked_query_text,
    r.status,
    r.cpu_time,
    r.logical_reads,
    r.start_time
FROM sys.dm_exec_requests r
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id <> 0
ORDER BY r.wait_time DESC;

To also see the blocking session’s query text, extend the query with a self-join:

-- Extended: blocked session + blocker query text side by side
SELECT
    r.session_id                         AS blocked_spid,
    r.blocking_session_id                AS blocking_spid,
    r.wait_type,
    r.wait_time                          AS wait_time_ms,
    r.wait_resource,
    DB_NAME(r.database_id)               AS database_name,
    LEFT(t_blocked.text, 300)            AS blocked_query,
    LEFT(t_blocker.text, 300)            AS blocker_query
FROM sys.dm_exec_requests                r
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t_blocked
LEFT JOIN sys.dm_exec_requests           r2
    ON r2.session_id = r.blocking_session_id
OUTER APPLY sys.dm_exec_sql_text(r2.sql_handle) t_blocker
WHERE r.blocking_session_id <> 0
ORDER BY r.wait_time DESC;

When to Use Each Query

Query 1 — Lock Detail View

  • You need to know exactly which object is locked
  • You need the lock mode (S, X, U, IS, IX)
  • You need both query texts in one row
  • Blocking involves row or page-level locks

Query 2 — Recursive Chain

  • Deep blocking chains with many sessions
  • You need to see every level of the chain
  • You want to confirm who the true lead blocker is
  • Post-incident analysis of a complex chain

Query 3 — Quick Check

  • First check during an incident — runs instantly
  • You need wait type and wait resource fast
  • You just need to confirm blocking exists
  • Monitoring scripts and health checks

All Three Together

  • Run Query 3 first to confirm and quantify blocking
  • Run Query 2 to find the lead blocker SPID
  • Run Query 1 to see the locked object and both queries
  • Decide whether to kill the lead blocker or wait

Reading the Results — Lock Modes Explained

Query 1 returns a request_mode column. Understanding lock modes helps diagnose why sessions are incompatible and waiting:

ModeNameWhat It MeansCompatible With
SSharedReading data — SELECTS, IS, U
XExclusiveModifying data — INSERT, UPDATE, DELETENothing
UUpdateIntent to update — read phase of updateS, IS
ISIntent SharedTable-level intent before row shared lockIS, S, U, IX, SIX
IXIntent ExclusiveTable-level intent before row exclusive lockIS, IX
SIXShared Intent ExclusiveTable shared, with intent to update rowsIS

The most common blocking scenario is an X lock (open transaction with an update) blocking an S lock (a read). The X lock is incompatible with everything — it holds until the transaction commits or rolls back.

What to Do After Finding the Lead Blocker

Once you have identified the lead blocker SPID, you have three options depending on the situation:

Option 1 — Wait

If the lead blocker is actively executing a legitimate long-running transaction (visible in sql_text), and the wait is within acceptable bounds, waiting is often the right answer. Killing a valid transaction forces a rollback which may take longer than waiting for it to complete.

Option 2 — Kill the Lead Blocker

If the session is idle (open transaction, no active query), or the blocking is causing unacceptable application impact, kill the lead blocker:

-- Kill the lead blocker — replace 17 with the actual SPID
KILL 17;

Always kill the lead blocker, not a mid-chain session. Killing a mid-chain session only releases the sessions it is directly blocking — the lead blocker continues to hold locks and block other sessions.

Option 3 — Investigate the Root Cause

After resolving the immediate incident, use the query text returned by these queries to investigate the root cause:

  • Is the blocking query missing an index that would reduce lock scope?
  • Is there an uncommitted transaction being held open by application logic?
  • Is lock escalation occurring due to a large number of row locks?
  • Would READ_COMMITTED_SNAPSHOT isolation level (RCSI) eliminate the blocking entirely?
-- Check if RCSI is enabled for a database (reduces read/write blocking significantly)
SELECT name, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = DB_NAME();
-- Enable RCSI on a database (test in non-production first)
ALTER DATABASE YourDatabase
SET READ_COMMITTED_SNAPSHOT ON;

References


Discover more from SQLYARD

Subscribe to get the latest posts sent to your email.

Leave a Reply

Discover more from SQLYARD

Subscribe now to keep reading and get access to the full archive.

Continue reading