Quick Answer
LCK_M_RX_X waits occur when a transaction needs exclusive access to both a specific row and the range between that row and the previous row in an index, typically during serializable isolation level operations or when acquiring range locks for phantom prevention. This wait indicates contention on range locks in ordered indexes and usually signals serializable transactions competing for overlapping key ranges.
Root Cause Analysis
The LCK_M_RX_X wait type manifests when SQL Server's lock manager cannot immediately grant a requested RangeX-X lock. This lock mode combines two distinct locking concepts: an exclusive lock on the current key value (X) and an exclusive range lock (RX) protecting the gap between the current key and the previous key in the index sort order.
SQL Server's lock manager maintains these range locks through the lock hash table, where each lock resource is identified by the database ID, object ID, index ID, and the specific key value. When a transaction requests RangeX-X locks, typically under serializable isolation level or when explicit locking hints force range lock acquisition, the lock manager must verify that no conflicting locks exist on both the specific key and the preceding range gap.
The scheduler thread requesting this lock transitions into a suspended state and registers the wait in sys.dm_os_waiting_tasks. The lock manager places the request into a conversion queue if a compatible lock exists on the resource, or into a grant queue if no lock exists. Range locks specifically protect against phantom reads by preventing other transactions from inserting keys within the protected range.
In SQL Server 2016 and later versions, lock escalation thresholds and memory pressure handling changed, affecting when range locks convert to table locks. SQL Server 2019 introduced intelligent query processing features that can influence locking patterns, while SQL Server 2022's parameter sensitive plan optimization can alter execution plans and subsequently change locking behavior for identical queries under different parameter values.
AutoDBA checks Range lock contention patterns, serializable isolation usage, and index fragmentation levels across your entire SQL Server instance in 60 seconds. Download the free diagnostic script and see what else needs attention.
Diagnostic Queries
-- Current blocking chain for RX-X locks
SELECT
wt.session_id,
wt.wait_type,
wt.wait_duration_ms,
wt.blocking_session_id,
wt.resource_description,
er.command,
er.database_id,
DB_NAME(er.database_id) as database_name,
st.text as sql_text
FROM sys.dm_os_waiting_tasks wt
INNER JOIN sys.dm_exec_requests er ON wt.session_id = er.session_id
CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) st
WHERE wt.wait_type = 'LCK_M_RX_X';
-- Lock details for sessions involved in RX-X contention
SELECT
tl.request_session_id,
tl.resource_type,
tl.resource_database_id,
tl.resource_associated_entity_id,
OBJECT_NAME(tl.resource_associated_entity_id, tl.resource_database_id) as object_name,
tl.resource_description,
tl.request_mode,
tl.request_status,
s.transaction_isolation_level,
CASE s.transaction_isolation_level
WHEN 1 THEN 'READ UNCOMMITTED'
WHEN 2 THEN 'READ COMMITTED'
WHEN 3 THEN 'REPEATABLE READ'
WHEN 4 THEN 'SERIALIZABLE'
WHEN 5 THEN 'SNAPSHOT' END as isolation_level_desc
FROM sys.dm_tran_locks tl
INNER JOIN sys.dm_exec_sessions s ON tl.request_session_id = s.session_id
WHERE tl.request_mode LIKE '%RangeX-X%'
OR tl.request_session_id IN (
SELECT DISTINCT blocking_session_id
FROM sys.dm_os_waiting_tasks
WHERE wait_type = 'LCK_M_RX_X'
);
-- Historical wait statistics for range lock contention
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
signal_wait_time_ms,
wait_time_ms - signal_wait_time_ms as resource_wait_time_ms,
CAST(100.0 * wait_time_ms / SUM(wait_time_ms) OVER() AS DECIMAL(5,2)) as pct_total_waits
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE 'LCK_M_RX%'
ORDER BY wait_time_ms DESC;
-- Index usage and contention points for affected objects
SELECT
OBJECT_NAME(i.object_id, DB_ID()) as table_name,
i.name as index_name,
i.type_desc,
ius.user_seeks,
ius.user_scans,
ius.user_lookups,
ius.user_updates,
ius.last_user_seek,
ius.last_user_update,
p.rows as row_count
FROM sys.indexes i
INNER JOIN sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id
INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
WHERE i.object_id IN (
SELECT DISTINCT resource_associated_entity_id
FROM sys.dm_tran_locks
WHERE request_mode LIKE '%RangeX-X%'
)
AND ius.database_id = DB_ID();
Fix Scripts
Identify and terminate long-running serializable transactions
-- WARNING: This script terminates active sessions. Test thoroughly in development.
-- Only run during maintenance windows or after confirming with application teams.
DECLARE @session_id int;
DECLARE kill_cursor CURSOR FOR
SELECT DISTINCT wt.blocking_session_id
FROM sys.dm_os_waiting_tasks wt
INNER JOIN sys.dm_exec_sessions s ON wt.blocking_session_id = s.session_id
WHERE wt.wait_type = 'LCK_M_RX_X'
AND s.transaction_isolation_level = 4 -- SERIALIZABLE
AND DATEDIFF(minute, s.last_request_start_time, GETDATE()) > 5; -- Running > 5 minutes
OPEN kill_cursor;
FETCH NEXT FROM kill_cursor INTO @session_id;
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT 'Killing session: ' + CAST(@session_id AS VARCHAR(10));
EXEC('KILL ' + @session_id);
FETCH NEXT FROM kill_cursor INTO @session_id;
END;
CLOSE kill_cursor;
DEALLOCATE kill_cursor;
This script terminates blocking sessions running under serializable isolation level for more than 5 minutes. Use extreme caution as this will rollback active transactions.
Enable snapshot isolation to reduce range lock contention
-- Enable snapshot isolation database options
-- This requires exclusive database access briefly during ALTER DATABASE
ALTER DATABASE [YourDatabaseName] SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE [YourDatabaseName] SET READ_COMMITTED_SNAPSHOT ON;
-- Verify snapshot isolation is enabled
SELECT
name,
snapshot_isolation_state_desc,
is_read_committed_snapshot_on
FROM sys.databases
WHERE name = 'YourDatabaseName';
Enabling snapshot isolation eliminates range locks for read operations but increases tempdb usage. Monitor tempdb space after implementation.
Add covering indexes to reduce lock escalation
-- Template for creating covering indexes on frequently contested ranges
-- Replace table_name, existing_key_columns, and include_columns based on workload analysis
CREATE NONCLUSTERED INDEX [IX_TableName_Covering_RangeLock]
ON [dbo].[table_name] ([existing_key_columns])
INCLUDE ([frequently_selected_columns])
WITH (
FILLFACTOR = 90, -- Leave space for inserts
PAD_INDEX = ON, -- Apply fillfactor to intermediate pages
ONLINE = ON, -- Build online if Enterprise Edition
MAXDOP = 4 -- Limit parallelism during build
);
Covering indexes reduce the need for key lookups that can trigger additional range locks. Monitor index maintenance overhead after creation.
AutoDBA generates fix scripts like these automatically, with impact estimates and rollback SQL included.
Prevention
Configure applications to use read committed snapshot isolation instead of serializable isolation level where phantom reads are not critical business requirements. This eliminates range lock acquisition for most read operations while maintaining transactional consistency.
Implement query hints strategically using NOLOCK for reporting queries that can tolerate dirty reads, or READPAST for queue-processing patterns where skipping locked rows is acceptable. Avoid WITH (SERIALIZABLE) hints unless absolutely required for business logic.
Design indexes with appropriate fillfactor settings (85-90%) on frequently updated tables to reduce page splits that can intensify range lock contention. Monitor index fragmentation weekly and rebuild indexes showing logical fragmentation above 30%.
Establish connection pooling with proper timeout settings to prevent abandoned connections from holding range locks indefinitely. Configure query timeout values between 30-60 seconds for OLTP workloads to prevent runaway queries from blocking range lock requests.
Monitor sys.dm_os_wait_stats daily for LCK_M_RX_X trends and correlate spikes with deployment schedules or batch processing windows. Set up automated alerts when range lock waits exceed 10% of total wait time, indicating systematic contention requiring architectural review.
Need hands-on help?
Dealing with persistent lck_m_rx_x issues across your environment? Samix Technology provides hands-on SQL Server performance consulting with 15+ years of production DBA experience.