Quick Answer
LCK_M_X occurs when a task waits to acquire an exclusive lock on a resource, typically during INSERT, UPDATE, or DELETE operations. This wait type signals blocking where one transaction prevents others from accessing data, often indicating concurrency problems or long-running transactions that require immediate attention.
Root Cause Analysis
LCK_M_X waits originate from SQL Server's lock manager when a session requests an exclusive lock but cannot acquire it because another session holds a conflicting lock on the same resource. The lock manager maintains a lock hash table that tracks all active locks, and when a conflict occurs, the requesting session enters a wait state on the lock resource.
Exclusive locks conflict with all other lock types (shared, update, exclusive, intent locks), making them the most restrictive. The lock manager queues waiting requests in FIFO order, but lock compatibility matrix checks occur before granting locks. These waits commonly manifest during high-concurrency OLTP workloads where multiple sessions compete for the same rows or pages.
Resource-level granularity affects wait duration significantly. Row-level locks (RID) typically resolve faster than page locks (PAG) or table locks (TAB). Key locks (KEY) on clustered indexes behave similarly to RID locks but apply to the key structure rather than physical row location.
SQL Server 2016 introduced query store, which helps identify problematic query patterns contributing to locking. SQL Server 2019's intelligent query processing features like adaptive joins can sometimes alter execution plans in ways that change locking behavior. SQL Server 2022's parameter-sensitive plan optimization may reduce some blocking scenarios by creating more appropriate execution plans for different parameter values.
The lock escalation threshold (5,000 locks) remains consistent across versions, but SQL Server 2019+ provides better visibility into escalation events through extended events. Memory-optimized tables in SQL Server 2014+ eliminate most locking waits entirely through optimistic concurrency control.
AutoDBA checks blocking detection, lock escalation thresholds, and transaction isolation levels across your entire SQL Server instance in 60 seconds. Download the free diagnostic script and see what else needs attention.
Diagnostic Queries
-- Identify current blocking chains and wait resources
SELECT
r.session_id,
r.wait_type,
r.wait_time,
r.wait_resource,
r.blocking_session_id,
s.program_name,
s.login_name,
t.text,
qp.query_plan
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) qp
WHERE r.wait_type = 'LCK_M_X'
ORDER BY r.wait_time DESC;
-- Find lock resource details and blocking hierarchy
WITH BlockingChain AS (
SELECT
session_id,
blocking_session_id,
wait_type,
wait_resource,
wait_time,
0 as Level
FROM sys.dm_exec_requests
WHERE blocking_session_id = 0 AND session_id IN (
SELECT DISTINCT blocking_session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id > 0
)
UNION ALL
SELECT
r.session_id,
r.blocking_session_id,
r.wait_type,
r.wait_resource,
r.wait_time,
bc.Level + 1
FROM sys.dm_exec_requests r
JOIN BlockingChain bc ON r.blocking_session_id = bc.session_id
)
SELECT * FROM BlockingChain
WHERE wait_type = 'LCK_M_X' OR session_id IN (
SELECT blocking_session_id FROM BlockingChain WHERE wait_type = 'LCK_M_X'
);
-- Analyze historical lock waits and patterns
SELECT TOP 20
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
signal_wait_time_ms,
wait_time_ms / waiting_tasks_count AS avg_wait_time_ms
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE 'LCK_M_%'
ORDER BY wait_time_ms DESC;
-- Identify tables with frequent lock escalations
SELECT
OBJECT_NAME(ddloh.object_id) AS table_name,
i.name AS index_name,
ddloh.lock_escalation_count,
ddloh.lock_escalation_attempt_count
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ddloh
JOIN sys.indexes i ON ddloh.object_id = i.object_id
AND ddloh.index_id = i.index_id
WHERE ddloh.lock_escalation_count > 0
ORDER BY ddloh.lock_escalation_count DESC;
-- Check for long-running transactions causing blocking
SELECT
s.session_id,
s.login_name,
s.program_name,
s.host_name,
t.transaction_id,
t.transaction_begin_time,
DATEDIFF(SECOND, t.transaction_begin_time, GETDATE()) AS transaction_duration_seconds,
t.transaction_state,
t.transaction_type
FROM sys.dm_tran_session_transactions st
JOIN sys.dm_tran_active_transactions t ON st.transaction_id = t.transaction_id
JOIN sys.dm_exec_sessions s ON st.session_id = s.session_id
WHERE DATEDIFF(SECOND, t.transaction_begin_time, GETDATE()) > 300
ORDER BY transaction_duration_seconds DESC;
Fix Scripts
Kill blocking session (emergency only)
-- CAUTION: This terminates a connection and rolls back the transaction
-- Use only when blocking session is confirmed problematic and non-critical
-- Replace @blocking_session_id with actual session ID from diagnostic queries
DECLARE @blocking_session_id INT = 54; -- Replace with actual SPID
DECLARE @kill_command NVARCHAR(50) = 'KILL ' + CAST(@blocking_session_id AS NVARCHAR(10));
-- Verify session details before executing
SELECT
session_id,
login_name,
program_name,
host_name,
last_request_start_time
FROM sys.dm_exec_sessions
WHERE session_id = @blocking_session_id;
-- Uncomment next line only after verification
-- EXEC sp_executesql @kill_command;
Enable READ_COMMITTED_SNAPSHOT for database
-- Reduces blocking by using row versioning instead of shared locks
-- Requires exclusive access to database during execution
-- Test thoroughly as this changes fundamental locking behavior
USE master;
GO
-- Check if tempdb has sufficient space for version store
SELECT
DB_NAME(database_id) AS database_name,
type_desc,
size * 8 / 1024 AS size_mb
FROM sys.master_files
WHERE database_id = 2; -- tempdb
-- Enable RCSI (requires single-user mode momentarily)
ALTER DATABASE [YourDatabaseName] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
ALTER DATABASE [YourDatabaseName] SET READ_COMMITTED_SNAPSHOT ON;
ALTER DATABASE [YourDatabaseName] SET MULTI_USER;
-- Verify setting
SELECT name, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = 'YourDatabaseName';
Reduce lock escalation thresholds
-- Disable lock escalation on frequently contended tables
-- Use when specific tables consistently escalate to table locks
-- Monitor memory usage as this increases lock manager overhead
USE [YourDatabaseName];
GO
-- Identify top escalation candidates first
SELECT TOP 10
OBJECT_NAME(object_id) AS table_name,
lock_escalation_attempt_count,
lock_escalation_count
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL)
WHERE lock_escalation_attempt_count > 0
ORDER BY lock_escalation_count DESC;
-- Disable escalation on specific problem table
ALTER TABLE [dbo].[YourProblemTable]
SET (LOCK_ESCALATION = DISABLE);
-- Alternative: Set to TABLE level only (not AUTO which includes partition)
-- ALTER TABLE [dbo].[YourProblemTable] SET (LOCK_ESCALATION = TABLE);
Optimize problematic queries
-- Add missing indexes to reduce lock duration and scope
-- Run after identifying blocking queries from diagnostic scripts
-- Generate index recommendations for blocking queries
SELECT DISTINCT
mid.statement AS table_name,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns,
mid.user_seeks,
mid.user_scans,
mid.avg_total_user_cost,
'CREATE NONCLUSTERED INDEX IX_' +
REPLACE(REPLACE(PARSENAME(mid.statement,1), '[', ''), ']', '') +
'_' + FORMAT(NEWID(), 'N') +
' ON ' + mid.statement +
' (' + ISNULL(mid.equality_columns, '') +
CASE WHEN mid.equality_columns IS NOT NULL AND mid.inequality_columns IS NOT NULL
THEN ',' ELSE '' END +
ISNULL(mid.inequality_columns, '') + ')' +
ISNULL(' INCLUDE (' + mid.included_columns + ')', '') AS create_index_statement
FROM sys.dm_db_missing_index_details mid
JOIN sys.dm_db_missing_index_groups mig ON mid.index_handle = mig.index_handle
JOIN sys.dm_db_missing_index_group_stats migs ON mig.index_group_handle = migs.group_handle
WHERE mid.database_id = DB_ID()
AND mid.avg_total_user_cost > 10
ORDER BY mid.avg_total_user_cost DESC;
AutoDBA generates fix scripts like these automatically, with impact estimates and rollback SQL included.
Prevention
Configure blocked process threshold to 10-30 seconds using sp_configure 'blocked process threshold' to receive alerts before blocking becomes critical. Implement regular monitoring of sys.dm_os_wait_stats to track LCK_M_X trends over time.
Design transactions to follow the principle of shortest duration possible. Access tables in consistent order across all procedures to prevent deadlocks that can exacerbate exclusive lock waits. Consider READ_COMMITTED_SNAPSHOT isolation for OLTP workloads to reduce reader-writer blocking.
Implement proper indexing strategies focusing on covering indexes for frequently executed queries. Partition large tables to reduce lock scope and enable partition-level operations. Use filtered indexes on frequently queried subsets to minimize lock resource contention.
Configure query timeout values appropriately (30-60 seconds for OLTP) to prevent runaway queries from holding locks indefinitely. Monitor tempdb space closely when using snapshot isolation levels as version store growth can cause performance degradation.
Establish connection pooling with reasonable timeout values to prevent abandoned connections from holding locks. Use application-level queuing for high-contention scenarios rather than allowing database-level blocking to cascade.
Review execution plans quarterly for scan operations on large tables that could benefit from index tuning. Implement lock monitoring through extended events to capture detailed blocking information for recurring issues.
Need hands-on help?
Dealing with persistent lck_m_x issues across your environment? Samix Technology provides hands-on SQL Server performance consulting with 15+ years of production DBA experience.