SQL Server sleeping head blocker: the idle session holding a lock and an open transaction
The most dangerous blocking shape in SQL Server does not look active. Sessions pile up behind a head blocker, batch requests stall, and transactions per second drop. CPU and I/O are idle. The engine looks healthy on resource metrics, but queries are timing out.
When you query sys.dm_exec_requests for the blocker, you find nothing. The session_id that everyone is waiting on does not appear in the requests DMV because the head blocker has no active request. It is sleeping. It is, however, holding locks under an open transaction that never committed, never rolled back, and never will on its own.
This is almost always an application bug or an abandoned interactive session. A connection with an open transaction was returned to the pool, a query timeout fired without XACT_ABORT ON, or someone left an SSMS window open with a BEGIN TRAN and walked away. The locks will not release until the transaction ends. The chain will not clear until the locks release. Killing the session triggers a rollback that may take as long as the original work.
What this means
Locks in SQL Server are tied to transactions. A session that takes a lock under a transaction holds it until the transaction commits or rolls back. If the session finishes its current batch without ending the transaction, SQL Server keeps the transaction open and keeps the locks. The session then goes to sleep waiting for the next command from the client. From the engine’s perspective, the session is idle. From the perspective of every session waiting on those locks, the session is the problem.
An active head blocker (a session running a long query that takes locks) eventually finishes its work, releases the locks, and the chain clears. A sleeping head blocker has nothing left to do. It will not finish. It is waiting for the application or the user to send the next command, and that next command may never come.
The blocking DMVs reflect this asymmetry. sys.dm_exec_requests shows blocked sessions but no row for the head blocker itself. sys.dm_exec_sessions shows the head blocker with status = 'sleeping'. That combination is the signature. The longer it persists and the more sessions back up behind it, the closer you are to worker thread exhaustion: new connections cannot be serviced and the instance appears unresponsive even though CPU and I/O are both low.
flowchart TD
A[Blocked sessions in dm_exec_requests] --> B{Head blocker row in dm_exec_requests?}
B -- Yes --> C[Active blocker: long query or escalation]
B -- No --> D[Suspect sleeping head blocker]
D --> E{Open transaction on suspect session?}
E -- No --> F[Investigate other lock sources]
E -- Yes --> G[Sleeping head blocker confirmed]
G --> H[KILL triggers rollback]
H --> I{ADR enabled on this database?}
I -- Yes --> J[Rollback near-instant on SQL Server 2019+]
I -- No --> K[Rollback may take long - monitor with STATUSONLY]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Connection returned to pool mid-transaction | program_name is the driver (.Net SqlClient, JDBC, HikariCP); host_name is an app server | Application code around transaction boundaries and Dispose/Close patterns |
Query timeout without XACT_ABORT ON | Application logged a timeout exception but the session is still on the instance holding locks | SET XACT_ABORT in stored procedure definitions and connection settings |
| Abandoned SSMS session | program_name = 'Microsoft SQL Server Management Studio', host_name is a workstation | Who has SSMS open against this instance right now |
| ORM SaveChanges delayed under app-tier pressure | Head blocker from the app server, transaction begin and commit split across requests | App server CPU, GC, and request logs around the time blocking started |
SET IMPLICIT_TRANSACTIONS ON left enabled | Session opened an implicit transaction on a SELECT and never committed | Connection settings and session-level SET options |
Quick checks
These are read-only and safe to run on a live instance.
-- Find current blocking chains and head blockers
SELECT
r.session_id AS blocked_session,
r.blocking_session_id AS blocker,
r.wait_type,
r.wait_time / 1000 AS wait_seconds,
DB_NAME(r.database_id) AS database_name,
t.text AS blocked_query_text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id <> 0
ORDER BY r.wait_time DESC;
-- Identify head blockers (sessions others wait on but that are not blocked themselves)
SELECT
s.session_id,
s.login_name,
s.host_name,
s.program_name,
s.status,
s.last_request_start_time,
s.last_request_end_time,
t.text AS last_query_text,
(SELECT COUNT(*) FROM sys.dm_exec_requests r2
WHERE r2.blocking_session_id = s.session_id) AS sessions_blocked
FROM sys.dm_exec_sessions s
CROSS APPLY sys.dm_exec_sql_text(s.most_recent_sql_handle) t
WHERE s.session_id IN (
SELECT DISTINCT blocking_session_id FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0
)
AND s.session_id NOT IN (
SELECT session_id FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0
)
ORDER BY sessions_blocked DESC;
The signature of the pattern: the head blocker has status = 'sleeping', a stale last_request_end_time (often minutes or hours in the past), and a large sessions_blocked count.
-- Confirm there is an open transaction on the suspect session
DBCC OPENTRAN;
DBCC OPENTRAN returns the oldest active transaction per database. If the suspect session’s SPID matches the output, you have confirmation. Note that DBCC OPENTRAN shows the oldest transaction per database and may miss read-only or snapshot transactions.
-- Find sleeping sessions with open transactions
SELECT
s.session_id,
s.login_name,
s.host_name,
s.program_name,
s.last_request_end_time,
tst.open_transaction_count
FROM sys.dm_exec_sessions s
JOIN sys.dm_tran_session_transactions tst ON s.session_id = tst.session_id
WHERE s.status = 'sleeping'
AND tst.open_transaction_count > 0
ORDER BY s.last_request_end_time;
Sessions in this list that have not made a request in minutes are the candidates for a sleeping head blocker. A sleeping session with no open transaction is harmless; the open_transaction_count > 0 filter is what matters.
How to diagnose it
Confirm the head blocker is sleeping. The head blocker query above is the source of truth. If
statusis anything other thansleeping, you have an active blocker and a different remediation path.Confirm an open transaction. Cross-reference the SPID against
DBCC OPENTRANoutput, or use thesys.dm_tran_session_transactionsjoin. Theopen_transaction_countcolumn should be greater than zero.Capture the input buffer. The
most_recent_sql_handlefrom the head blocker query may return NULL when the offending batch has aged out of cache. Try:
-- Get the last statement the head blocker submitted
SELECT event_info
FROM sys.dm_exec_input_buffer(<session_id>, NULL);
A NULL or empty input buffer is common with this pattern. The transaction was likely opened by an earlier batch that has since been cleaned from the cache.
Inspect what locks are held. The blocked sessions’ wait types and resources tell you what the head blocker is pinning.
LCK_M_S,LCK_M_X,LCK_M_U, andLCK_M_SCH_Mmap to shared, exclusive, update, and schema-modification locks respectively.Quantify the impact before acting. Count blocked sessions, note the oldest wait, and check whether transactions per second have dropped relative to batch requests per second. This tells you whether to kill now or wait for the application owner.
Watch for the cascade into worker exhaustion. If active workers are climbing toward
max_workers_countorTHREADPOOLwaits have started appearing insys.dm_os_wait_stats, you are minutes from the instance refusing new connections. The Dedicated Admin Connection (DAC) bypasses normal connection limits and is your fallback when normal connections fail.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| Blocking chain depth and oldest wait | Quantifies user-visible impact | Chain > 5 sessions, oldest wait > 60 seconds, head blocker sleeping |
| Transactions/sec vs batch requests/sec ratio | Queries are arriving but not committing | Ratio dropping while batch requests stay flat or rise |
LCK_M_* wait types in sys.dm_os_wait_stats | Aggregate lock pressure | Lock waits climbing into the top five waits by total wait time |
THREADPOOL waits in sys.dm_os_wait_stats | Cascade approaching worker exhaustion | Any sustained nonzero value |
| Worker thread utilization | Prelude to system unresponsiveness | Active workers sustained above 80% of max_workers_count |
| Batch requests/sec trend | Throughput drop signals user impact | Sustained 0.5x baseline or worse with no upstream explanation |
log_reuse_wait_desc = ACTIVE_TRANSACTION | Side-effect of long open transactions | Long-running transaction is also blocking log truncation |
Fixes
Kill the session after impact assessment
When the head blocker is sleeping with an open transaction and the blocking chain is causing throughput impact, the only path to immediate relief is KILL. This is not free.
-- Terminate the session - this triggers rollback
KILL <session_id>;
Rollback duration is roughly proportional to the work the transaction has done, not the time it has been open. A transaction that did 30 seconds of heavy updates can take 30 seconds or longer to roll back. The session shows KILLED/ROLLBACK during this period. Monitor progress with:
-- Watch rollback progress without affecting it
KILL <session_id> WITH STATUSONLY;
Killing is the right call when blocked sessions are accumulating and the application owner cannot be reached. Killing is the wrong call when the transaction has done very large modifications on a database without Accelerated Database Recovery enabled and the rollback will likely outlast the original blocking impact. Always check sys.dm_tran_session_transactions before killing a long-running writer.
On SQL Server 2017 CU24, 2019 CU8, and 2016 SP2 CU15 and later, a bug that could leave killed sessions stuck indefinitely in rollback waiting on TRANSACTION_MUTEX or XACT_OWN_TRANSACTION is fixed. If you are running older builds, treat KILL on a long-running transaction with extra caution.
Enable Accelerated Database Recovery (SQL Server 2019+) for faster rollback
ADR fundamentally changes rollback behavior. With ADR enabled, rollback is near-instantaneous regardless of transaction size. A KILL against a sleeping head blocker on an ADR-enabled database completes in seconds rather than minutes or hours.
ADR is available in SQL Server 2019 and later, Azure SQL Database, and Azure SQL Managed Instance. It is not available in SQL Server 2017 or earlier. Enabling ADR on an existing database has overhead in the form of a persistent version store and should be tested, but for OLTP workloads where this pattern recurs, it converts a multi-hour rollback risk into a non-event.
Fix the application’s transaction handling
The permanent fix is in the application tier.
- Set
XACT_ABORT ONfor every stored procedure and every connection that takes transactions. WithXACT_ABORT ON, a timeout or cancel rolls back the transaction automatically instead of leaving it open on a sleeping session. This is the single highest-leverage change. - Ensure connection-pool dispose paths always roll back. In .NET, wrap
SqlConnectionandSqlTransactioninusingblocks, or useTransactionScopecorrectly. In JDBC, pairsetAutoCommit(false)with explicitcommit()orrollback()in a finally block. - Configure the pool’s connection-validation query to detect and clean up leaked transactions where the pool supports it. A query like
IF @@TRANCOUNT > 0 ROLLBACK;run when a connection is returned to the pool catches the common case. - Investigate ORM patterns like Entity Framework’s
SaveChangesthat hold transactions open across the request. Under app-tier CPU or GC pressure, the commit is delayed and the session on SQL Server appears sleeping with an open transaction. The root cause is the app tier, not the database.
Walk away from abandoned SSMS sessions
For interactive sessions, the fix is procedural. Do not leave BEGIN TRAN uncommitted in SSMS. Use SET XACT_ABORT ON in query windows so a closed window rolls back instead of orphaning the transaction. Configure idle session timeouts so abandoned SSMS windows cannot hold locks indefinitely.
Prevention
- Make
SET XACT_ABORT ONthe default in every stored procedure template and every application connection string. - Wrap every transaction in deterministic rollback-on-error patterns at the application layer. Treat uncaught exceptions as rollback triggers.
- Add a connection-validation query to your pool configuration that includes
IF @@TRANCOUNT > 0 ROLLBACK. - Schedule periodic checks of
sys.dm_tran_session_transactionsjoined withsys.dm_exec_sessionsfor sessions wherestatus = 'sleeping',open_transaction_count > 0, andlast_request_end_timeis older than a threshold. Alert on it. - Enable ADR on SQL Server 2019+ OLTP databases where the pattern recurs.
- Audit application code that holds transactions across network calls or external dependencies. Those calls are the most common source of sleeping sessions with open transactions.
How Netdata helps
- The blocking chain and head blocker queries run at per-second cadence and persist over time. A sleeping head blocker is hard to miss when the chart shows the same SPID blocking dozens of sessions for minutes.
- Per-second wait statistics surface the
LCK_M_*spike as the chain forms, before it cascades intoTHREADPOOLwaits and worker exhaustion. - Correlating batch requests per second (still arriving), transactions per second (dropping), and worker thread utilization gives a fast read on whether you are looking at a sleeping head blocker versus an active slow query.
- Trend charts of
LCK_M_*wait time as a fraction of total waits make regression visible. When the ratio climbs after a deploy, an application code change is the likely cause. - Persistent history lets you reconstruct the timeline: when the sleeping session started, how long it held locks, and what else was running on the instance at the same time.
Netdata’s Microsoft SQL Server monitoring brings these signals together at per-second resolution.
Related guides
- SQL Server blocking chains: finding the head blocker before workers run out
- SQL Server buffer cache hit ratio low: when the working set no longer fits in memory
- SQL Server user connections climbing: connection pool leaks and retry storms
- SQL Server CPU utilization high: telling query load apart from a bad plan
- SQL Server CXPACKET and CXCONSUMER waits: parallelism, MAXDOP, and what is actually wrong
- SQL Server Error 1205: transaction was deadlocked and chosen as the deadlock victim
- SQL Server Error 701: there is insufficient system memory to run this query
- SQL Server Error 9002: the transaction log for the database is full
- SQL Server high compilations per second: plan cache pollution and CPU burn
- How Microsoft SQL Server actually works in production: a mental model for operators
- SQL Server log autogrow stall: why every write pauses while the log file grows
- SQL Server log backups missing: the full-recovery log that grows forever






