Lock Escalation, and Why You Shouldn’t Disable It!
Lock escalation is one of those things that shows up in blocking investigations, gets blamed for the problem, and then gets “fixed” in the worst possible way — by disabling it. This post is about why that’s almost always wrong, and what to do instead.
Lock escalation is the process of converting many fine-grained locks (such as row or page locks) into table locks. Microsoft SQL Server dynamically determines when to perform lock escalation. When making this decision, SQL Server takes into consideration the number of locks that are held on a particular scan, the number of locks that are held by the whole transaction, and the memory that is being used for locks in the system as a whole. Typically, SQL Server’s default behavior results in lock escalation occurring only at those points where it would improve performance or when you must reduce excessive system lock memory to a more reasonable level. However, some application or query designs may trigger lock escalation at a time when it is not desirable, and the escalated table lock may block other users.
Lock escalation is an optimization, not a bug. SQL Server trades many fine-grained locks for a single coarse lock because holding thousands of individual lock structures costs real memory and CPU. When you see lock escalation causing blocking, the escalation itself is rarely the problem — it’s a symptom of something else: a missing index, an oversized transaction, or a query plan that touches far more rows than it needs to.
This article covers the common root causes and what to do about them. The goal is to fix the actual problem so escalation either doesn’t happen or doesn’t matter.
You can disable lock escalation per table:
|
1 |
ALTER TABLE dbo.[YourTable] SET (LOCK_ESCALATION = DISABLE); |
Or globally with trace flag 1211 (which disables escalation for the entire instance — never do this in production). There’s also trace flag 1224, which is slightly less dangerous: it disables escalation based on lock count but still allows escalation under memory pressure. (TF 1211 disables both count-based and memory-pressure-based escalation — TF 1211 takes precedence if both are set.) Neither flag is a real solution.
Let’s look at the actual problems and how to fix them.

Unnecessary Index Scans
The most common trigger for lock escalation is a query that scans a large portion of a table when it should be doing a seek. Lock escalation kicks in when a single statement acquires roughly 5,000 locks on the current table or index — that’s per statement, not per transaction. Escalation can also be triggered by lock memory pressure, even if the per-object count is below 5,000. A scan across a million-row table blows past that threshold almost immediately.
You’ll see this pattern: a SELECT with a WHERE clause that looks selective but doesn’t match any index, so the optimizer picks a scan. The scan acquires shared locks row by row (or page by page, depending on the isolation level and plan), hits the 5,000-lock threshold, and SQL Server escalates to a shared table lock. Now every concurrent writer is blocked.
If you disable lock escalation as a way around this type of concurrency problem, you haven’t solved anything — you’ve just traded one problem for a worse one. Instead of a table lock that blocks writers briefly during the scan, you now have a session holding thousands (or millions) of individual row locks for the duration of the statement. Those locks consume memory, inflate sys.dm_tran_locks, and can still block other sessions depending on your isolation level. And the scan is still slow.
The fix is to add the missing index. Check the query plan — look for scans where you expect seeks, and look at the missing index DMVs:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
SELECT mid.[statement] AS [table_name] , migs.[avg_user_impact] , migs.[user_seeks] , mid.[equality_columns] , mid.[inequality_columns] , mid.[included_columns] FROM sys.dm_db_missing_index_group_stats AS migs INNER JOIN sys.dm_db_missing_index_groups AS mig ON mig.[index_group_handle] = migs.[group_handle] INNER JOIN sys.dm_db_missing_index_details AS mid ON mid.[index_handle] = mig.[index_handle] WHERE migs.[avg_user_impact] > 80 ORDER BY migs.[avg_user_impact] DESC; |
Once the query seeks instead of scans, it acquires locks only on the rows it actually needs. The lock count stays well under the escalation threshold, and the blocking disappears — without touching LOCK_ESCALATION.
Batch Your Large Operations
Large UPDATE or DELETE statements are the other classic escalation trigger. If you need to update 500,000 rows, a single statement will acquire locks on all of them, blow past the 5,000-lock threshold, and escalate to an exclusive table lock. Every other session is locked out until the statement finishes.
The fix is batching. Process rows in chunks small enough to stay under the escalation threshold:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
DECLARE @BatchSize int = 4000; DECLARE @RowsAffected int = 1; DECLARE @cmd nvarchar(max); SET @cmd = N' UPDATE TOP (' + CONVERT(nvarchar(11), @BatchSize, 0) + N') dbo.[YourTable] SET [Status] = ''Archived'' WHERE [Status] = ''Pending'' AND [CreatedDate] < DATEADD(YEAR, -2, GETDATE()); '; WHILE @RowsAffected > 0 BEGIN EXEC (@cmd); SET @RowsAffected = @@ROWCOUNT; END; |
Each batch targets 4,000 rows, but the actual lock count can exceed that depending on indexes and access paths — nonclustered index maintenance, key locks, and intent locks all add up. The batch size leaves headroom below the escalation threshold. The batch finishes, releases its locks, and lets other sessions through before the next batch starts. You stay under the escalation threshold, and other queries can interleave between batches.
A few things to watch:
WHERE clause is indexed. If each batch triggers a table scan to find its rows, you’re back to the same problem. The predicate columns need an index.WAITFOR DELAY '00:00:00.100' between batches on busy OLTP systems if you want to throttle the load. Whether this is necessary depends on how hot the table is.For deletes, the pattern is the same — just replace UPDATE with DELETE.
Lock Memory Pressure
Every lock SQL Server holds is a data structure in memory. The exact size depends on the SQL Server version and the lock mode, but it’s in the neighborhood of 64–128 bytes per lock resource. Microsoft’s older documentation cited 96 bytes; some versions use more, some less. The internal LOCK_HEADER, LOCK_OWNER, and LOCK_RESOURCE structures have changed across versions, so don’t treat any single number as gospel. The point is: each lock costs real memory.
If you disable lock escalation on a table and then run a statement that touches a million rows, you’re holding a million individual lock structures. At ~96 bytes each, that’s roughly 92 MB of memory consumed just by locks. On a server with plenty of RAM, that might not matter. On SQL Server Standard Edition — capped at 128 GB buffer pool (or 64 GB prior to SQL Server 2016 SP1) — that memory is consumed inside SQL Server and can reduce memory available for the buffer pool and other caches. Ninety-two megabytes of lock structures means 92 MB less data and index pages cached in memory.
You can see current lock memory consumption in sys.dm_os_memory_clerks:
|
1 2 3 4 5 6 |
SELECT [type] , [pages_kb] / 1024 AS [lock_memory_mb] FROM sys.dm_os_memory_clerks WHERE [type] = 'OBJECTSTORE_LOCK_MANAGER' ORDER BY [pages_kb] DESC; |
And you can see individual locks with sys.dm_tran_locks:
|
1 2 3 4 5 6 7 8 |
SELECT [resource_type] , [request_mode] , COUNT(*) AS [lock_count] FROM sys.dm_tran_locks WHERE [resource_type] NOT IN ('DATABASE') GROUP BY [resource_type], [request_mode] ORDER BY [lock_count] DESC; |
When lock escalation is working normally, SQL Server replaces thousands of row locks with a single table lock — freeing that memory. Disabling escalation means those row locks stick around for the entire duration of the statement. Under concurrent load from multiple sessions, the lock memory can grow fast.
When Lock Escalation Can’t Be Disabled
LOCK_ESCALATION is a table-level setting on user tables only. You cannot disable lock escalation on:
sys. tables, msdb system tables, and internal catalog tables. SQL Server manages locking on these internally and does not expose a LOCK_ESCALATION option.#temp tables — These don’t support ALTER TABLE ... SET (LOCK_ESCALATION = ...).ALTER TABLE, ALTER INDEX REBUILD, TRUNCATE TABLE) always acquire schema modification (Sch-M) locks at the table level regardless of the LOCK_ESCALATION setting. Some bulk operations under specific conditions also bypass the setting.The LOCK_ESCALATION setting only controls lock manager escalation behavior — the runtime decision to trade many row/page locks for a table lock. It doesn’t override situations where SQL Server requires a table lock by design.
What About LOCK_ESCALATION = AUTO?
The AUTO option was introduced in SQL Server 2008. (Table partitioning itself shipped in SQL Server 2005; AUTO came later to give partitioned tables a better escalation behavior.) Instead of escalating row/page locks to a table lock, AUTO allows SQL Server to escalate to HoBT level (Heap or B-Tree) rather than immediately to the whole table. In practice, this often means partition-level escalation for the clustered index or heap, but HoBT is the precise term.
|
1 |
ALTER TABLE dbo.[YourPartitionedTable] SET (LOCK_ESCALATION = AUTO); |
With AUTO, if a statement acquires 5,000 locks within a single partition, SQL Server escalates to a lock on that partition only. Other partitions remain accessible to concurrent sessions. This is genuinely useful for partitioned tables where different workloads target different partitions — for example, a sliding-window pattern where an ETL process loads data into the newest partition while reporting queries read from older partitions.
A few caveats:
AUTO only helps on partitioned tables. On a non-partitioned table, AUTO behaves identically to TABLE — there’s only one HoBT, so a HoBT lock is a table lock.AUTO can increase deadlock risk in some partitioned workloads where multiple sessions access different partitions in different orders. Monitor after enabling.AUTO is a reasonable choice when you have a partitioned table and you’ve verified that your workloads naturally isolate to different partitions. For non-partitioned tables, it’s meaningless.
Monitoring Lock Escalation
Before you change anything, measure. You need to know when escalation happens, which tables are affected, and which queries trigger it.
Extended Events are the best tool for this. The lock_escalation event fires every time SQL Server escalates:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
CREATE EVENT SESSION [LockEscalation] ON SERVER ADD EVENT sqlserver.lock_escalation ( ACTION ( sqlserver.sql_text , sqlserver.session_id , sqlserver.database_name , sqlserver.username ) ) ADD TARGET package0.event_file ( SET filename = N'LockEscalation.xel' , max_file_size = 50 -- MB ) WITH (MAX_DISPATCH_LATENCY = 5 SECONDS); ALTER EVENT SESSION [LockEscalation] ON SERVER STATE = START; |
The event captures the object being escalated, the escalation type (TABLE, HoBT), the lock mode, and the number of locks that triggered it. Pair that with the sql_text action and you know exactly which query to go fix.
Fix the Root Cause
Lock escalation exists because holding tens of thousands of fine-grained locks is expensive and, in most workloads, unnecessary. When you see escalation causing blocking, the escalation is doing its job — it’s telling you that a query is touching too many rows.
The pattern is almost always the same:
- A query scans more rows than it needs to → add a better index.
- A bulk operation modifies too many rows in one shot → batch it.
- A long-running transaction holds locks for too long → shorten the transaction.
Disabling lock escalation doesn’t fix any of these. It just hides the symptom while introducing memory pressure, and it can make blocking worse under concurrency because now you have millions of fine-grained locks competing with each other instead of one clean table lock that finishes and releases quickly.
Fix the query. Fix the index. Fix the batch size. Let lock escalation do its job.