When dozens of threads hit the same query simultaneously on a cold cache, SQL Server compiles the same plan over and over — burning CPU, wasting resources, and degrading throughput right when your system is most vulnerable. This is a compilation storm, and SQL Server 2025 has a one-line fix for it.
What is a compilation storm?
Every query needs a compiled execution plan before SQL Server can run it. Plans are cached — but when the cache is cold (after a restart, failover, or deployment) and many threads fire the same query at once, each thread independently compiles the same plan:

One thread pays the compilation cost. Everyone else waits a millisecond and reuses for free.
Step-by-step demo script
The full script below walks through all 10 steps end to end. Run each section in order.
1) Create the database and tables
Sets up jbexecute with Customers, Products, Orders, and a CompilationMetrics table to store before/after results.
USE master;GOIF EXISTS (SELECT 1 FROM sys.databases WHERE name = N'jbexecute')BEGIN ALTER DATABASE jbexecute SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE jbexecute;ENDGOCREATE DATABASE jbexecute;GOALTER DATABASE jbexecute SET RECOVERY SIMPLE;GOUSE jbexecute;GOCREATE TABLE dbo.Customers( CustomerID INT NOT NULL IDENTITY(1,1) PRIMARY KEY, FirstName NVARCHAR(50) NOT NULL, LastName NVARCHAR(50) NOT NULL, Email NVARCHAR(100) NOT NULL, Region NVARCHAR(30) NOT NULL, CreatedDate DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME());GOCREATE TABLE dbo.Products( ProductID INT NOT NULL IDENTITY(1,1) PRIMARY KEY, ProductName NVARCHAR(100) NOT NULL, Category NVARCHAR(50) NOT NULL, UnitPrice DECIMAL(10,2) NOT NULL, StockQty INT NOT NULL DEFAULT 1000);GOCREATE TABLE dbo.Orders( OrderID INT NOT NULL IDENTITY(1,1) PRIMARY KEY, CustomerID INT NOT NULL REFERENCES dbo.Customers(CustomerID), ProductID INT NOT NULL REFERENCES dbo.Products(ProductID), Quantity INT NOT NULL, TotalAmount DECIMAL(12,2) NOT NULL, OrderStatus NVARCHAR(20) NOT NULL DEFAULT N'Pending', OrderDate DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME());GOCREATE TABLE dbo.CompilationMetrics( MetricID INT NOT NULL IDENTITY(1,1) PRIMARY KEY, Phase NVARCHAR(10) NOT NULL, CapturedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), CompileEvents INT NOT NULL, TotalDurationNs BIGINT NOT NULL, TotalDurationMs AS (TotalDurationNs / 1000000.), Notes NVARCHAR(500) NULL);
2) Seed sample Data
200 customers, 50 products, 5,000 orders.
;WITH cte AS( SELECT TOP 200 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n FROM sys.all_columns a CROSS JOIN sys.all_columns b)INSERT INTO dbo.Customers (FirstName, LastName, Email, Region)SELECT N'FirstName' + CAST(n AS NVARCHAR(10)), N'LastName' + CAST(n AS NVARCHAR(10)), N'user' + CAST(n AS NVARCHAR(10)) + N'@shop.com', CHOOSE((n % 5) + 1, N'North', N'South', N'East', N'West', N'Central')FROM cte;GO;WITH cte AS( SELECT TOP 50 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n FROM sys.all_columns)INSERT INTO dbo.Products (ProductName, Category, UnitPrice, StockQty)SELECT N'Product_' + CAST(n AS NVARCHAR(10)), CHOOSE((n % 6) + 1, N'Electronics', N'Clothing', N'Books', N'Home & Garden', N'Sports', N'Toys'), CAST((n * 7.99) AS DECIMAL(10,2)), 500 + (n * 10)FROM cte;GO;WITH cte AS( SELECT TOP 5000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n FROM sys.all_columns a CROSS JOIN sys.all_columns b)INSERT INTO dbo.Orders (CustomerID, ProductID, Quantity, TotalAmount, OrderStatus)SELECT (n % 200) + 1, (n % 50) + 1, (n % 10) + 1, CAST(((n % 50) + 1) * 7.99 * ((n % 10) + 1) AS DECIMAL(12,2)), CHOOSE((n % 4) + 1, N'Pending', N'Shipped', N'Delivered', N'Cancelled')FROM cte;
3) Set feature OFF and create Extended Events session
Captures query_post_compilation_showplan (compilation time) and sql_batch_completed (execution) to an .xel file.
-- Disable for BEFORE testALTER DATABASE SCOPED CONFIGURATION SET ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY = OFF;ALTER DATABASE SCOPED CONFIGURATION SET OPTIMIZED_SP_EXECUTESQL = OFF;GOUSE master;GOIF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = N'Demo_CompilationStorm') DROP EVENT SESSION Demo_CompilationStorm ON SERVER;GOCREATE EVENT SESSION [Demo_CompilationStorm] ON SERVERADD EVENT sqlserver.query_post_compilation_showplan( ACTION ( sqlserver.sql_text, sqlserver.database_name, sqlserver.client_app_name, sqlserver.session_id ) WHERE ([sqlserver].[database_name] = N'jbexecute')),ADD EVENT sqlserver.sql_batch_completed( ACTION (sqlserver.sql_text, sqlserver.database_name) WHERE ([sqlserver].[database_name] = N'jbexecute' AND [duration] > 0))ADD TARGET package0.event_file( SET filename = N'C:\temp\JBExecute\Demo_CompilationStorm.xel', max_file_size = 1024, max_rollover_files = 5)WITH (MAX_DISPATCH_LATENCY = 5 SECONDS, TRACK_CAUSALITY = ON);GOALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = START;GO
4) Create the workload file (workload.sql)
Save this as a .sql file. It is what ostress will fire on every thread and iteration.
DECLARE @custID INT;DECLARE @prodID INT;DECLARE @qty INT;DECLARE @amount DECIMAL(12,2);DECLARE @status NVARCHAR(20);DECLARE @sql NVARCHAR(MAX);DECLARE @params NVARCHAR(MAX);SET @sql = N' INSERT INTO dbo.Orders (CustomerID, ProductID, Quantity, TotalAmount, OrderStatus) VALUES (@cid, @pid, @qty, @amt, @stat); UPDATE dbo.Products SET StockQty = StockQty - @qty WHERE ProductID = @pid AND StockQty >= @qty; SELECT o.OrderID, c.FirstName + '' '' + c.LastName AS CustomerName, p.ProductName, o.Quantity, o.TotalAmount, o.OrderStatus FROM dbo.Orders o JOIN dbo.Customers c ON c.CustomerID = o.CustomerID JOIN dbo.Products p ON p.ProductID = o.ProductID WHERE o.CustomerID = @cid ORDER BY o.OrderDate DESC;';SET @params = N'@cid INT, @pid INT, @qty INT, @amt DECIMAL(12,2), @stat NVARCHAR(20)';SET @custID = (ABS(CHECKSUM(NEWID())) % 200) + 1;SET @prodID = (ABS(CHECKSUM(NEWID())) % 50) + 1;SET @qty = (ABS(CHECKSUM(NEWID())) % 5) + 1;SET @amount = CAST(@prodID * 7.99 * @qty AS DECIMAL(12,2));SET @status = N'Pending';EXEC sp_executesql @sql, @params, @cid = @custID, @pid = @prodID, @qty = @qty, @amt = @amount, @stat = @status;
5) Run BEFORE workload with ostress
Clear the plan cache first. Demo servers only. Then fire ostress.
-- Clear plan cache (DEMO SERVER ONLY)ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;GO-- Run from command prompt:-- ostress -S"YOUR_SERVER" -E -i"C:\temp\JBExecute\workload.sql" -n100 -r5 -q -djbexecuteALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = STOP;GOWAITFOR DELAY '00:00:05';
6) Capture and save BEFORE metrics
Reads the .xel file and stores compile event count and total compile duration into CompilationMetrics.
USE jbexecute;GO;WITH events AS( SELECT n.value('(@name)[1]', 'NVARCHAR(100)') AS event_name, n.value('(data[@name="duration"]/value)[1]', 'BIGINT') AS duration_ns, n.value('(action[@name="database_name"]/value)[1]', 'NVARCHAR(128)') AS db_name FROM ( SELECT CAST(event_data AS XML) AS event_xml FROM sys.fn_xe_file_target_read_file( N'C:\temp\JBExecute\Demo_CompilationStorm*.xel', NULL, NULL, NULL) ) src CROSS APPLY src.event_xml.nodes('//event') AS t(n))SELECT 'BEFORE (No Feature)' AS Phase, event_name, COUNT(*) AS CompileEventCount, SUM(duration_ns) AS TotalDuration_ns, SUM(duration_ns) / 1000000. AS TotalDuration_ms, AVG(duration_ns) AS AvgDuration_ns, MAX(duration_ns) AS MaxDuration_nsFROM eventsWHERE event_name = 'query_post_compilation_showplan'AND db_name = 'jbexecute'GROUP BY event_name;-- Save to metrics table;WITH events AS( SELECT n.value('(@name)[1]', 'NVARCHAR(100)') AS event_name, n.value('(data[@name="duration"]/value)[1]', 'BIGINT') AS duration_ns FROM ( SELECT CAST(event_data AS XML) AS event_xml FROM sys.fn_xe_file_target_read_file( N'C:\temp\JBExecute\Demo_CompilationStorm*.xel', NULL, NULL, NULL) ) src CROSS APPLY src.event_xml.nodes('//event') AS t(n))INSERT INTO dbo.CompilationMetrics (Phase, CompileEvents, TotalDurationNs, Notes)SELECT 'BEFORE', COUNT(*), ISNULL(SUM(duration_ns), 0), 'SQL Server without Optimized sp_executesql — concurrent compilation storm'FROM eventsWHERE event_name = 'query_post_compilation_showplan';
7) Enable OPTIMIZED_SP_EXECUTESQL
One line. No downtime. No application changes.
USE jbexecute;GOALTER DATABASE SCOPED CONFIGURATION SET ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY = ON;ALTER DATABASE SCOPED CONFIGURATION SET OPTIMIZED_SP_EXECUTESQL = ON;GO-- VerifySELECT name, value, value_for_secondaryFROM sys.database_scoped_configurationsWHERE name IN (N'OPTIMIZED_SP_EXECUTESQL', N'ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY');GO-- Fresh start for AFTER testALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;GOALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = STOP;GOALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = START;GO-- Run ostress again with identical parameters, then:ALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = STOP;GOWAITFOR DELAY '00:00:05';
8) Capture and save AFTER metrics
USE jbexecute;GO;WITH events AS( SELECT n.value('(@name)[1]', 'NVARCHAR(100)') AS event_name, n.value('(data[@name="duration"]/value)[1]', 'BIGINT') AS duration_ns, n.value('(action[@name="database_name"]/value)[1]', 'NVARCHAR(128)') AS db_name FROM ( SELECT CAST(event_data AS XML) AS event_xml FROM sys.fn_xe_file_target_read_file( N'C:\Temp\JBExecute\Demo_CompilationStorm*.xel', NULL, NULL, NULL) ) src CROSS APPLY src.event_xml.nodes('//event') AS t(n))SELECT 'AFTER (Optimized sp_executesql ON)' AS Phase, event_name, COUNT(*) AS CompileEventCount, SUM(duration_ns) AS TotalDuration_ns, SUM(duration_ns) / 1000000. AS TotalDuration_ms, AVG(duration_ns) AS AvgDuration_ns, MAX(duration_ns) AS MaxDuration_nsFROM eventsWHERE event_name = 'query_post_compilation_showplan'AND db_name = 'jbexecute'GROUP BY event_name;;WITH events AS( SELECT n.value('(@name)[1]', 'NVARCHAR(100)') AS event_name, n.value('(data[@name="duration"]/value)[1]', 'BIGINT') AS duration_ns FROM ( SELECT CAST(event_data AS XML) AS event_xml FROM sys.fn_xe_file_target_read_file( N'C:\Temp\JBExecute\Demo_CompilationStorm*.xel', NULL, NULL, NULL) ) src CROSS APPLY src.event_xml.nodes('//event') AS t(n))INSERT INTO dbo.CompilationMetrics (Phase, CompileEvents, TotalDurationNs, Notes)SELECT 'AFTER', COUNT(*), ISNULL(SUM(duration_ns), 0), 'SQL Server 2025 with OPTIMIZED_SP_EXECUTESQL = ON — serialized compilation'FROM eventsWHERE event_name = 'query_post_compilation_showplan';
9) Compare before vs after
Shows compile event count, total compile duration, and average compile time per event — all with % reduction.
-- Phase detailSELECT Phase, CapturedAt, CompileEvents, TotalDurationNs, TotalDurationMs AS TotalDuration_ms, CASE WHEN Phase = 'BEFORE' THEN 'Baseline — N compilations per burst' ELSE 'Optimized — 1 compilation per unique batch' END AS Description, NotesFROM dbo.CompilationMetricsORDER BY MetricID;-- Reduction summarySELECT b.CompileEvents AS BEFORE_CompileEvents, a.CompileEvents AS AFTER_CompileEvents, b.CompileEvents - a.CompileEvents AS Compilations_Saved, CAST(100.0 * (b.CompileEvents - a.CompileEvents) / NULLIF(b.CompileEvents, 0) AS DECIMAL(5,1)) AS Compilations_Pct_Reduction, b.TotalDurationMs AS BEFORE_TotalCompileDuration_ms, a.TotalDurationMs AS AFTER_TotalCompileDuration_ms, b.TotalDurationMs - a.TotalDurationMs AS CompileDuration_ms_Saved, CAST(100.0 * (b.TotalDurationMs - a.TotalDurationMs) / NULLIF(b.TotalDurationMs, 0) AS DECIMAL(5,1)) AS CompileDuration_Pct_Reduction, CASE WHEN b.CompileEvents > 0 THEN b.TotalDurationNs / b.CompileEvents END AS BEFORE_AvgCompile_ns, CASE WHEN a.CompileEvents > 0 THEN a.TotalDurationNs / a.CompileEvents END AS AFTER_AvgCompile_ns, CASE WHEN b.CompileEvents > 0 AND a.CompileEvents > 0 THEN (b.TotalDurationNs / b.CompileEvents) - (a.TotalDurationNs / a.CompileEvents) END AS AvgCompile_ns_Saved, CAST( 100.0 * ( (b.TotalDurationNs / NULLIF(b.CompileEvents, 0)) - (a.TotalDurationNs / NULLIF(a.CompileEvents, 0)) ) / NULLIF(b.TotalDurationNs / NULLIF(b.CompileEvents, 0), 0) AS DECIMAL(5,1)) AS AvgCompile_Pct_ReductionFROM (SELECT TOP 1 * FROM dbo.CompilationMetrics WHERE Phase = 'BEFORE' ORDER BY MetricID DESC) b CROSS JOIN (SELECT TOP 1 * FROM dbo.CompilationMetrics WHERE Phase = 'AFTER' ORDER BY MetricID DESC) a;GO
10) Cleanup
ALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = STOP;DROP EVENT SESSION [Demo_CompilationStorm] ON SERVER;GOUSE master;ALTER DATABASE jbexecute SET SINGLE_USER WITH ROLLBACK IMMEDIATE;DROP DATABASE jbexecute;GO
Things to know before enabling in production
- SQL Server 2025 only. OPTIMIZED_SP_EXECUTESQL is not available on 2022 or earlier. ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY is available from 2022 onwards.
- Waiting threads add latency. Non-first threads wait for the compiling thread. Negligible in most cases — but test carefully if you have genuinely slow-compiling queries.
- sp_executesql only. Ad-hoc SQL, stored procedures, and batches where the SQL text varies between calls are not covered by this feature.
- Does not fix bad plans. This serializes compilation — it does not improve plan quality. Use Query Store for plan stability alongside this.
- Test before production. Validate with Extended Events as shown in this demo. Every workload is different.
Watch the Full Demo
I’ve recorded a complete walkthrough of this setup on my YouTube channel JBSWiki. If you’re a visual learner, go check it out!
👉 Watch here: https://www.youtube.com/watch?v=3BF9wn_IXWQ
Thank You,
Thank You,
Vivek Janakiraman
Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided “AS IS” with no warranties, and confers no rights.
