Imagine this: a session in SQL Server isn’t running any queries, it’s sitting idle, yet it’s blocking everyone else.
That, my friends, is an idle blocker.
In this post we’ll cover:
- What an idle blocker actually is
- How it happens
- Why it causes so much chaos
- How to detect it (sp_who2, DBCC INPUTBUFFER, DMVs)
- Advanced ways to inspect locks and transactions
- How to kill multiple idle blockers safely
- And finally — how to prevent them in the first place
🧠 What Is an Idle Blocker?
An idle blocker is a SQL Server session (SPID) that:
- Isn’t running any active queries
- Still holds locks on rows, pages, or tables
- Blocks other sessions from moving forward because it hasn’t released those locks
You’ll usually see it as a session in SLEEPING status that sits at the top of a blocking chain.
🔁 How It Happens
Idle blockers are usually caused by bad client behavior:
- Uncommitted Transactions
A client starts a transaction and never commits or rolls it back. SQL Server holds locks until it knows the outcome.
BEGIN TRANSACTION;
-- Forgot to COMMIT or ROLLBACK
-- Locks are held indefinitely
2. Open Connections Not Closed
In app code (C#, Java, etc.), if you don’t dispose of the connection, the session can linger with locks.
// BAD - missing connection disposal
var conn = new SqlConnection(connString);
conn.Open();
// run a query
// forgot to close or dispose!
3. Lazy Loading in ORMs
Long-lived DbContexts (Entity Framework) or lazy-loaded queries can keep connections open longer than expected.
🚧 Why It’s a Problem
Here’s the chain reaction:
- SPID 52: starts a transaction, updates a row, doesn’t commit.
- SPID 60: tries to read the row — gets blocked.
- SPID 70: waits on SPID 60, and so on.
SPID 52 isn’t doing anything useful, but it’s holding locks. That’s why idle blockers are silent but deadly.
🔎 How to Detect Idle Blockers
Step 1: Find Who’s Blocking Whom
SELECT
r.session_id AS BlockedSession,
r.blocking_session_id AS BlockerSession,
s.status,
s.login_name,
s.host_name,
s.program_name,
r.wait_type,
r.wait_time,
r.wait_resource
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
WHERE r.blocking_session_id <> 0;
Step 2: Identify the Idle Blocker
SELECT
s.session_id,
s.status,
s.login_name,
s.host_name,
s.last_request_start_time,
s.last_request_end_time,
r.text AS LastCommand
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests req
ON s.session_id = req.session_id
LEFT JOIN sys.dm_exec_connections c
ON s.session_id = c.session_id
OUTER APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) r
WHERE s.session_id IN (
SELECT DISTINCT blocking_session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id != 0
)
AND req.session_id IS NULL; -- only idle blockers
Step 3: Peek Into Its Last Command (Quick Check)
DBCC INPUTBUFFER(<SPID>);
This old-school command gives you a one-line snapshot of what the session last executed.
It’s fast, lightweight, and often enough to tell you whether the blocker is sitting on an open transaction or just waiting.
🔬 A Richer View of Idle Blockers
If you need more detail, use this DMV query:
- Age of the open transaction (
tran_minutes) - Log usage (
tran_log_mb) - Last command text
- Which sessions are blocked
WITH blocking AS (
SELECT er.session_id AS victim_session_id,
er.blocking_session_id AS blocker_session_id
FROM sys.dm_exec_requests er
WHERE er.blocking_session_id <> 0
),
idle_blocker_roots AS (
SELECT DISTINCT b.blocker_session_id AS session_id
FROM blocking b
LEFT JOIN sys.dm_exec_requests r
ON r.session_id = b.blocker_session_id
WHERE r.session_id IS NULL
),
x AS (
SELECT
s.session_id,
s.status,
s.login_name,
s.host_name,
s.program_name,
s.last_request_start_time,
s.last_request_end_time,
s.open_transaction_count,
c.client_net_address,
DB_NAME(dt.database_id) AS database_name,
at.transaction_begin_time,
DATEDIFF(MINUTE, at.transaction_begin_time, SYSDATETIME()) AS tran_minutes,
(dt.database_transaction_log_bytes_used/1048576.0) AS tran_log_mb,
txt.text AS last_command
FROM idle_blocker_roots r
JOIN sys.dm_exec_sessions s ON s.session_id = r.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
OUTER APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) txt
LEFT JOIN sys.dm_tran_session_transactions st ON st.session_id = s.session_id
LEFT JOIN sys.dm_tran_active_transactions at ON at.transaction_id = st.transaction_id
LEFT JOIN sys.dm_tran_database_transactions dt ON dt.transaction_id = at.transaction_id
)
SELECT
x.session_id,
x.status,
x.login_name,
x.host_name,
x.program_name,
x.database_name,
x.open_transaction_count,
x.transaction_begin_time,
x.tran_minutes,
x.tran_log_mb,
x.client_net_address,
STUFF((
SELECT ', ' + CONVERT(varchar(10), b.victim_session_id)
FROM (SELECT DISTINCT victim_session_id
FROM blocking
WHERE blocker_session_id = x.session_id) b
FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, ''
) AS blocking_victims,
x.last_command
FROM x
ORDER BY x.tran_minutes DESC, x.session_id;
Optional: See Locks Held
SELECT
l.request_session_id AS session_id,
DB_NAME(l.resource_database_id) AS database_name,
l.resource_type,
l.request_mode,
l.request_status,
COUNT(*) AS lock_count
FROM sys.dm_tran_locks l
WHERE l.request_session_id IN (
SELECT DISTINCT er.blocking_session_id
FROM sys.dm_exec_requests er
WHERE er.blocking_session_id <> 0
EXCEPT
SELECT session_id FROM sys.dm_exec_requests
)
GROUP BY l.request_session_id, l.resource_database_id, l.resource_type, l.request_mode, l.request_status
ORDER BY session_id, database_name;
💣 How to Kill Idle Blockers
The simplest form:
KILL <SPID>;
But this only handles one blocker at a time. If you’ve got several, use a script that finds them all and generates KILL commands.
⚡ Kill Multiple Idle Blockers Safely
Here’s a pattern that:
- Finds only root idle blockers
- Excludes your session and critical logins/programs
- Lets you filter by transaction age
- Either prints or executes the KILLs
DECLARE @execute BIT = 0; -- 0 = print, 1 = run
DECLARE @min_tran_age_minutes INT = 5;
DECLARE @ExcludeLogin TABLE (login_name sysname PRIMARY KEY);
INSERT @ExcludeLogin (login_name) VALUES ('sa');
DECLARE @ExcludeProgram TABLE (program_name sysname PRIMARY KEY);
INSERT @ExcludeProgram (program_name) VALUES ('SQLAgent - TSQL JobStep');
WITH blocking AS (
SELECT er.session_id AS victim_session_id,
er.blocking_session_id AS blocker_session_id
FROM sys.dm_exec_requests er
WHERE er.blocking_session_id <> 0
),
idle_blocker_roots AS (
SELECT DISTINCT b.blocker_session_id AS session_id
FROM blocking b
LEFT JOIN sys.dm_exec_requests r ON r.session_id = b.blocker_session_id
WHERE r.session_id IS NULL
),
blockers AS (
SELECT s.session_id,
s.login_name,
s.host_name,
s.program_name,
at.transaction_begin_time,
DATEDIFF(MINUTE, at.transaction_begin_time, SYSDATETIME()) AS tran_minutes
FROM idle_blocker_roots r
JOIN sys.dm_exec_sessions s ON s.session_id = r.session_id
LEFT JOIN sys.dm_tran_session_transactions st ON st.session_id = s.session_id
LEFT JOIN sys.dm_tran_active_transactions at ON at.transaction_id = st.transaction_id
WHERE s.is_user_process = 1 AND s.session_id <> @@SPID
)
SELECT * INTO #KillList
FROM blockers b
WHERE (b.tran_minutes >= @min_tran_age_minutes OR b.tran_minutes IS NULL)
AND NOT EXISTS (SELECT 1 FROM @ExcludeLogin x WHERE x.login_name = b.login_name)
AND NOT EXISTS (SELECT 1 FROM @ExcludeProgram x WHERE x.program_name = b.program_name);
IF NOT EXISTS (SELECT 1 FROM #KillList)
BEGIN
PRINT 'No idle blockers matched.';
RETURN;
END
SELECT session_id, login_name, host_name, program_name, tran_minutes
FROM #KillList;
DECLARE @cmd NVARCHAR(MAX) = N'';
SELECT @cmd = @cmd + N'KILL ' + CAST(session_id AS varchar(10)) + ';' + CHAR(13)
FROM #KillList;
IF @execute = 1
EXEC sys.sp_executesql @cmd;
ELSE
SELECT KillCommand = 'KILL ' + CAST(session_id AS varchar(10)) + ';'
FROM #KillList;
🛡️ How to Prevent Idle Blockers
In Application Code
- Always close connections (
usingin C#). - Set reasonable
CommandTimeoutandConnectionTimeout. - Always
COMMITorROLLBACKtransactions. - Avoid long-lived DbContexts and lazy loading in ORMs.
In SQL Server
- Turn on READ_COMMITTED_SNAPSHOT to reduce blocking with row versioning.
- Monitor blocking regularly (alerts/jobs for sessions blocking >30 sec).
- Log frequent blockers and fix at the source (bad app code, ORM misuse).
🧯 Summary
| Problem | Cause | Fix |
|---|---|---|
| Idle Blocker | Session holding locks but doing nothing | Kill safely, fix app code |
| Open Connection | App forgot to close | Use using / dispose properly |
| Uncommitted Tran | BEGIN TRAN but no COMMIT | Always close transactions |
| ORM Issues | Lazy loading / long-lived contexts | Use short-lived contexts |
📌 Final Thoughts
Idle blockers are silent performance killers. They don’t show up as “running queries,” so they’re easy to overlook — but they can grind your workload to a halt.
With a few smart queries and disciplined app coding practices, you can:
- Detect idle blockers in real time
- Kill them safely (even in bulk)
- And most importantly, prevent them from happening again.
Discover more from SQLYARD
Subscribe to get the latest posts sent to your email.


