DBA Scripts: Get Blocking Summary

🔧Part of the DBA-Tools Project, copy/paste SQL Server scripts and health checks.In: Performance & TroubleshootingBlocking & Locking

When Everything Looks Busy But Nothing Is Moving

Blocking is one of the most common causes of a SQL Server that suddenly looks frozen. Applications start timing out, users report that everything has stopped responding, and the server itself looks fine: CPU is low, disk queues are calm, nothing is crashing. Dozens of sessions are simply queued behind one that never let go of a lock.

One script, one question, and it is the question an incident actually turns on: is there blocking right now, and who is causing it. Everything else can wait until you have that answer. When the answer turns out to be a chain rather than a single session, Blocking Chains picks the story up, walking the chain recursively and recovering the statement even from a blocker that is asleep rather than running.


Why Blocking Matters

SQL Server uses locking to enforce transaction isolation, and most of the time that is invisible: a session holds a lock for a few milliseconds and moves on. The trouble starts when one session holds a lock far longer than expected, through an open transaction that never commits, a long-running batch, or an application that forgot to close a connection, and everything else that wants the same data queues up behind it.

  • A single forgotten transaction can block dozens of sessions within seconds, turning one problem into what looks like a full outage
  • The sessions doing the waiting are innocent. Killing them does nothing, because the fix is always upstream at the session holding the lock
  • Chains can run several sessions deep, C behind B behind A, so the head blocker is not always the session you first land on
  • It is one of the fastest checks to run during an incident, because the answer is usually obvious the moment you see who the head blocker is

When to Run This Script

  • The moment users or an application report SQL Server hanging or timing out
  • Routine SQL Server health checks
  • Before escalating a slowness incident to infrastructure or networks, to rule blocking in or out first
  • After deploying application changes that touch transaction handling

The Script

One row per head blocker, ordered by how much damage each is doing.

✓ Verified
  • Tested on: SQL Server 2025 (RTM CU8, 17.0.4075.5), Windows lab instance
  • Last verified: 2026-08-31 (run against real blocking created on the lab, not a manufactured example)
  • Permissions: VIEW SERVER STATE
  • Safety: read-only, impact low
Read only: it never ends a session. Killing is always your decision.
/*
Script Name : Get-BlockingSummary
Category    : performance-troubleshooting
Purpose     : Head blockers with context — who is blocking, how many sessions, and what they are running.
Author      : Peter Whyte (https://sqldba.blog/dba-scripts-get-blocking-sessions/)
Requires    : VIEW SERVER STATE
*/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;

WITH blocked_counts AS (
    SELECT
        r.blocking_session_id,
        COUNT(*) AS blocked_session_count,
        MAX(r.wait_time) / 1000 AS max_wait_sec,
        SUM(r.wait_time) / 1000 AS total_wait_sec
    FROM sys.dm_exec_requests AS r
    WHERE r.blocking_session_id <> 0
    GROUP BY r.blocking_session_id
)
SELECT
    bc.blocking_session_id AS head_blocker_session_id,
    bc.blocked_session_count,
    bc.max_wait_sec,
    bc.total_wait_sec,
    s.login_name AS head_blocker_login,
    s.host_name AS head_blocker_host,
    s.program_name AS head_blocker_program,
    DB_NAME(s.database_id) AS head_blocker_database,
    s.open_transaction_count,
    r.wait_type AS head_blocker_wait_type,
    CAST(ISNULL(r.wait_time, 0) / 1000.0 AS DECIMAL(10,1)) AS head_blocker_wait_sec,
    SUBSTRING(ISNULL(qt.text, ''), 1, 500) AS head_blocker_statement
FROM blocked_counts AS bc
JOIN sys.dm_exec_sessions AS s ON s.session_id = bc.blocking_session_id
LEFT JOIN sys.dm_exec_requests AS r ON r.session_id = bc.blocking_session_id
LEFT JOIN sys.dm_exec_connections AS c ON c.session_id = bc.blocking_session_id
                                           AND r.session_id IS NULL
OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) AS qt
ORDER BY bc.blocked_session_count DESC, bc.max_wait_sec DESC;

For most incidents that is the whole job: it names the session, and you go and deal with it.

TipRun it twice, thirty seconds apart. One run says blocking exists; two say whether it is draining or building, which is the difference between waiting it out and intervening. Same session id both times with a rising blocked_session_count is the one to act on.

How To Run From The Repo

Clone DBA Tools, initialize and run it:

# Clone dba-tools repo:
git clone https://github.com/peterwhyte-lgtm/dba-tools

# Initialize environment:
cd dba-tools
.\Initialize-Environment.ps1

# Is there blocking right now, and who is the head blocker:
.\run.ps1 Get-BlockingSummary

This script lives in the repo at:


Example Output

Real blocking, created on the lab: one session held an uncommitted UPDATE on a row, and two more queued behind it.

SSMS results grid from Get-BlockingSummary showing two head blocker rows on one instance, the first waiting on WAITFOR with one open transaction and the second waiting on LCK_M_X with two, meaning that blocker is itself blocked.

Two rows, and only the first is worth acting on. It is the real head: WAITFOR, one open transaction, holding a lock while doing nothing at all. The second is listed as a blocker too, but its own wait type is LCK_M_X, which means it is blocked as well.

That is a chain caught in the act. Both sessions are blocking somebody, only one of them is the cause, and the difference between them is that single wait type column. Deal with the row on WAITFOR and the other resolves itself.


Understanding the Results

Seven columns decide what you do next. The rest are context for whoever you have to call.

head_blocker_session_id
blocked_session_count
Who everyone is queued behind, and how many. The waiting sessions are innocent, so this is the only id worth acting on. One is a nuisance; a count that climbs each time you rerun is an outage forming.Act when the count is climbing between runs. The queue is growing faster than it drains.
open_transaction_count
Open, uncommitted transactions held by the blocker. The fastest signal that a session is sitting on locks it no longer needs, and Open Transactions goes deeper when you need it.Act when this is 1 or more and the blocker is doing no visible work. That is the forgotten commit, and it will not clear on its own.
head_blocker_wait_type
What the blocker itself waits on. NULL means it is genuinely running something. A lock wait here means the blocker is blocked too, so this is a chain and the real culprit sits further up: Blocking Chains walks it.
max_wait_sec
total_wait_sec
The longest single wait, and the sum across everything queued. Total is the better measure of damage: ten sessions waiting three seconds hurts more than one waiting ten.
head_blocker_statement
The blocker’s SQL, trimmed to 500 characters, usually enough to name the application or job. It comes from sys.dm_exec_sql_text, which returns the whole batch rather than the statement executing now, so a long batch shows its opening lines rather than the line that took the lock.

Common Causes

  • An application transaction that does its work then waits on something else, a network call or a user prompt, before committing, holding the lock the whole time
  • A long-running report or batch reading under an isolation level that holds locks far longer than a simple SELECT would
  • A missing index forcing a scan that takes more locks, for longer, than the equivalent seek
  • An application bug where an exception path skips the commit or rollback, leaving the transaction open until the connection is killed or times out

How to Fix Blocking

Identify the head blocker from the summary. If open_transaction_count is above zero and the wait type suggests it is not doing anything useful, the transaction is probably stuck. Confirm what it is before ending it: read the statement text, and check sys.dm_tran_locks for the resources actually held.

Killing a genuine long-running transaction forces a rollback that can take as long as the transaction took to get there, and the locks stay held throughout. That is the right call for a forgotten transaction, not a first response. Troubleshoot SQL Server Blocking walks the whole decision.

For blocking that keeps recurring rather than a one-off, the durable fixes are:

  • Add the missing index, so the query holds locks for less time in the first place
  • Shorten the application transaction. Commit as soon as the work is logically done, and never hold one open across a network round trip you do not control
  • Review the isolation level. READ COMMITTED SNAPSHOT removes most reader and writer blocking by giving readers a versioned copy instead of a lock to wait on, and the locking and row versioning guide covers the trade-offs

Best Practices

  • Run the summary first on any report that the database is slow or hung, before assuming a resource problem. Blocking is among the fastest things to rule in or out
  • Run it twice before acting, so you know whether the queue is draining or growing
  • Keep application transactions short, and never hold one open across a round trip you do not control
  • Watch open_transaction_count specifically. It is the single fastest signal that a session is holding a lock it no longer needs

Frequently Asked Questions

What’s the difference between blocking and a deadlock?

Blocking is one session waiting for another to release a lock, and it resolves on its own once the blocker finishes or is killed. A deadlock is two or more sessions each holding a lock the other needs, with no way forward, so SQL Server detects the cycle and kills one automatically. Blocking needs you; a deadlock has already resolved itself by the time you read about it. Deadlock Summary covers that side.

Two rows came back. Which one do I deal with?

Read head_blocker_wait_type. A session listed as a blocker whose own wait type is itself a lock wait is blocked as well, so it is a symptom rather than the cause. The row that is not waiting on a lock is the real head, and clearing it usually clears the rest.

That is a chain. If it runs more than a node or two deep, Blocking Chains walks it properly rather than leaving you to read session ids off against each other.

Is killing the head blocker always safe?

No. Confirm what it is doing first, through sys.dm_tran_locks and the statement text. A legitimate long transaction such as a migration or an overnight batch will roll back when killed, and the rollback can take as long as the work took to get there, with the locks still held throughout. Killing is right for a genuinely stuck or forgotten transaction, not as a first response.

Why does head_blocker_statement show more than the statement that took the lock?

Because sys.dm_exec_sql_text returns the entire batch for the handle, not the single statement executing now. On a long procedure or a multi statement batch you get the opening lines, which is often not the line holding the lock. It is usually enough to identify the application, which is what you need during an incident. When it is not, sys.dm_tran_locks carries the exact resources held.

The blocking has cleared. Can I still find out what caused it?

Not from this script. It reads live DMVs, so it shows only what is happening at the instant you run it, with no history behind it.

For blocking that comes and goes you need something capturing continuously: the blocking collector job in this toolkit, or an Extended Events session. Microsoft’s guide to understanding and resolving blocking covers the monitoring options in full.


Related Scripts

You may also find these scripts useful:


Summary

Blocking is one of the most common reasons SQL Server suddenly looks unresponsive, and one of the fastest to diagnose once you know where to look: find the head blocker, not the sessions waiting on it. This answers whether there is a problem and who is causing it in seconds, which is all you need in order to act.

Run the summary as a first response any time something looks hung. It takes seconds and either rules blocking in or out immediately.

Comments

Leave a Reply

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