SQL Server 2022 Cumulative Update 25 (build 16.0.4255.1, released 20 May 2026) ships with a known issue that Microsoft has now carried, unfixed, through more than four years and more than a dozen cumulative updates. The built-in SESSION_CONTEXT function can return wrong results or throw an access violation when it runs inside a parallel query plan on a pooled connection. Most SQL Server estates are not affected at all. The minority that do use the feature can hit a genuine data-correctness risk rather than just a crash, and in the worst case, under Row-Level Security, that means one tenant's query silently returning another tenant's rows. This post explains what the bug actually is, why Microsoft has not resolved it, how likely your organisation is to be exposed, and it finishes with a read-only script you can run before any patch to find out for certain.
What is the SESSION_CONTEXT parallel plan bug?
SESSION_CONTEXT() reads a key from the current session's context bag, the values you set with sp_set_session_context. The bug is narrow and specific. Microsoft's own known-issues note puts it plainly: you might encounter access violation exceptions or wrong results when the SESSION_CONTEXT function runs within a parallel execution plan and the session is reset for reuse.
That last clause is the important one. A session is reset for reuse every time a connection goes back to a connection pool and is handed to the next request, which is exactly what application connection pooling does through sp_reset_connection. So the failure window only opens when all three of these line up at the same time:
- A query reads from
SESSION_CONTEXT(). - The plan goes parallel, meaning its cost clears the cost threshold for parallelism and MAXDOP is greater than 1.
- The session has just been reset for reuse from a pool.
When those conditions meet, you can get one of two outcomes. The crash variant produces an access violation and a dump file, which is loud and obvious. The wrong-results variant is silent, and as we will see, that is the one that should worry you.
Why hasn't Microsoft fixed it after all this time?
This is the part worth understanding, because it is not neglect so much as a trade-off Microsoft got stuck in. There was originally a wrong-results bug with SESSION_CONTEXT in parallel plans. Microsoft shipped a fix for it in SQL Server 2019 (15.x) CU14. That fix was later found to cause access violation exceptions under certain conditions. In other words, the correctness fix introduced a stability regression.
Rather than ship another fix that might trade one failure mode for another, Microsoft documented a trace flag workaround and left the underlying behaviour in place. The known-issues table tells the whole story in three columns. Date discovered: January 2022. Status: Has workaround. Date resolved: blank. That has been the state through every cumulative update since, which is why the same entry keeps reappearing in release notes, including SQL Server 2022 CU25, and why it persists in SQL Server 2019, 2022 and 2025 alike. The applies-to scope is SQL Server 2019 (15.x) CU14 and later, which by version lineage means every build of SQL Server 2022 inherits it.
How prevalent is it, and how likely is a client to be affected?
The honest answer is that it depends entirely on whether you use SESSION_CONTEXT at all, and for most estates the real-world risk is low to nil. Here is how it breaks down.
If you do not use SESSION_CONTEXT, which is the majority of installations, the risk is zero. The feature has to be in the code path for the bug to fire, so patching to CU25 changes nothing for you.
If you do use it, the dominant real-world use case is Row-Level Security (RLS). The application sets a tenant or user identifier with sp_set_session_context, and the RLS predicate function reads it back with SESSION_CONTEXT() to filter rows. Multi-tenant SaaS schemas, auditing context, and current-user patterns running under connection pooling are the typical shapes. These are the estates to take seriously, and the wrong-results variant is the dangerous one here. Under RLS, a wrong result can mean returning another tenant's rows. That is a silent data-correctness and potential data-leakage issue, not merely a crash you would notice immediately.
Even among SESSION_CONTEXT users, the read still has to land in a parallel plan. RLS predicates are usually inline functions evaluated cheaply per row, and the lookup query itself often stays serial. Exposure rises with larger reporting or analytic queries against RLS-protected tables, where the optimiser is more likely to go parallel and where connection pooling guarantees sessions are constantly reset for reuse. So the practical risk concentrates in one profile: multi-tenant applications using RLS, with pooled connections, running heavier parallel queries against the protected tables.
How do you mitigate it? Trace flag 11042
The documented mitigation is trace flag 11042, which can be set as a startup, global, or session flag. It forces SESSION_CONTEXT to execute serially, which prevents it from participating in parallel query plans and removes both failure modes, the access violation and the wrong results. For most workloads that read a single scalar session value the serial cost is negligible, because the function call itself is cheap.
You want the flag set in two ways. Add -T11042 to the instance startup parameters in SQL Server Configuration Manager so the mitigation survives restarts, and turn it on globally at runtime so it takes effect immediately without waiting for a restart:
-- Immediate, global effect (no restart). Pair this with -T11042 in the
-- instance startup parameters so it is not lost on the next restart.
DBCC TRACEON (11042, -1);
-- Confirm it is active globally
DBCC TRACESTATUS (11042, -1);
There is a related flag worth recognising. Trace flag 9432 disables the SQL Server 2019 CU14 fix entirely, reverting to the pre-fix behaviour. For nearly all situations 11042 is the right choice, because it addresses both failure modes rather than swapping which one you are exposed to.
One thing worth confirming on a per-instance basis is whether 11042 is already enabled from a prior remediation. Some organisations turned it on years ago when the original access violation first bit them, and may have it running globally at runtime but never recorded it in the startup parameters. If that is the case the mitigation silently disappears on the next restart, which is the kind of surprise you want to minimise during a patch window.
How do you check whether your instances are exposed?
Before updating any client to a recent SQL Server 2022 cumulative update, run a quick check for SESSION_CONTEXT usage. A search across RLS security policies, predicate functions, modules and application batches tells you in minutes whether an instance is even in scope. For any instance that is, set trace flag 11042 globally as part of the patch runbook. It is a safe, documented, low-cost mitigation that closes both failure modes. For instances with no usage, no action is needed, and this known issue is not a reason to hold off on the cumulative update. If you first need to confirm which build each instance is running, here is how to check your SQL Server version.
The script below automates exactly that gate. It is read-only. It scans every accessible user database for module and RLS usage, optionally scans the plan cache for ad hoc and application-side usage, and reports the current trace flag state, including the runtime-versus-startup distinction described above. Read the first result set: it returns one row per instance with the build level, a clear Status (PASS or ACTION REQUIRED), counts of where SESSION_CONTEXT was found, the trace flag state, and a one-line action. The detail result sets only appear when the instance is in scope, so a clean server stays a single tidy row.
/*======================================================================================
Detect_SESSION_CONTEXT_Usage.sql
--------------------------------------------------------------------------------------
Purpose : Pre-patch gate for the SESSION_CONTEXT parallel-plan known issue
(wrong results / access violation when SESSION_CONTEXT runs in a parallel
plan and the session is reset for reuse via connection pooling).
Applies to SQL Server 2019 CU14 and later, including all SQL Server 2022 CUs.
Documented mitigation is trace flag 11042 (forces serial execution).
Scope : Read-only. Scans every online, accessible user database on the instance for
module usage and Row-Level Security policies, optionally scans the plan cache,
and reports the current TF11042 state (runtime global + startup parameter).
Use : Run on each instance as a pre-patch check. Read the SUMMARY result set first.
Detail result sets follow for evidence.
Notes : Requires VIEW SERVER STATE (plan cache + sys.dm_server_registry) and db access.
Detection is text-based on module/query text. A module that references
SESSION_CONTEXT only in a comment will register as a (false) positive, which is
the safe direction for a gate. Set @ScanPlanCache = 0 on very large/busy
servers to skip the plan-cache pass.
Author : DBA Services
======================================================================================*/
SET NOCOUNT ON;
-----------------------------------------------------------------------------------------
-- Config
-----------------------------------------------------------------------------------------
DECLARE @ScanPlanCache bit = 1; -- 0 to skip the plan-cache scan
DECLARE @PlanCacheTopN int = 200; -- cap on plan-cache rows returned
-- Search patterns. Underscore is escaped so it is treated literally, not as a wildcard.
DECLARE @PatAny nvarchar(100) = N'%session[_]context%'; -- any reference
DECLARE @PatRead nvarchar(100) = N'%session[_]context(%'; -- the read function call
DECLARE @PatSet nvarchar(100) = N'%sp[_]set[_]session[_]context%'; -- the setter proc
-----------------------------------------------------------------------------------------
-- Staging
-----------------------------------------------------------------------------------------
IF OBJECT_ID('tempdb..#Modules') IS NOT NULL DROP TABLE #Modules;
IF OBJECT_ID('tempdb..#Policies') IS NOT NULL DROP TABLE #Policies;
IF OBJECT_ID('tempdb..#PlanCache') IS NOT NULL DROP TABLE #PlanCache;
IF OBJECT_ID('tempdb..#TF') IS NOT NULL DROP TABLE #TF;
CREATE TABLE #Modules (
DatabaseName sysname,
SchemaName sysname,
ObjectName sysname,
ObjectType nvarchar(60),
UsesSetContext bit,
UsesReadContext bit
);
CREATE TABLE #Policies (
DatabaseName sysname,
PolicySchema sysname,
PolicyName sysname,
PolicyEnabled bit,
IsSchemaBound bit,
TargetObject nvarchar(512),
PredicateType nvarchar(60),
Operation nvarchar(60),
PredicateDefinition nvarchar(max)
);
CREATE TABLE #PlanCache (
DatabaseName sysname NULL,
ObjectName nvarchar(512) NULL,
ExecutionCount bigint,
LastExecution datetime,
QuerySnippet nvarchar(600)
);
CREATE TABLE #TF (TraceFlag int, [Status] int, [Global] int, [Session] int);
-----------------------------------------------------------------------------------------
-- Per-database scan (modules + RLS). Each DB wrapped so one failure does not abort.
-----------------------------------------------------------------------------------------
DECLARE @db sysname, @sql nvarchar(max);
DECLARE db_cur CURSOR LOCAL FAST_FORWARD FOR
SELECT name
FROM sys.databases
WHERE state_desc = N'ONLINE'
AND database_id > 4 -- skip system DBs; drop this line to include them
AND source_database_id IS NULL -- skip snapshots
AND HAS_DBACCESS(name) = 1
AND DATABASEPROPERTYEX(name, N'Updateability') IN (N'READ_WRITE', N'READ_ONLY');
OPEN db_cur;
FETCH NEXT FROM db_cur INTO @db;
WHILE @@FETCH_STATUS = 0
BEGIN
BEGIN TRY
-- Modules referencing SESSION_CONTEXT (read function or setter proc)
SET @sql = N'USE ' + QUOTENAME(@db) + N';
SELECT DB_NAME(), s.name, o.name, o.type_desc,
CASE WHEN m.definition COLLATE Latin1_General_CI_AI LIKE @pSet THEN 1 ELSE 0 END,
CASE WHEN m.definition COLLATE Latin1_General_CI_AI LIKE @pRead THEN 1 ELSE 0 END
FROM sys.sql_modules m
JOIN sys.objects o ON m.object_id = o.object_id
JOIN sys.schemas s ON o.schema_id = s.schema_id
WHERE m.definition COLLATE Latin1_General_CI_AI LIKE @pAny;';
INSERT #Modules
EXEC sys.sp_executesql
@sql,
N'@pSet nvarchar(100), @pRead nvarchar(100), @pAny nvarchar(100)',
@pSet = @PatSet, @pRead = @PatRead, @pAny = @PatAny;
-- Row-Level Security policies and predicates (RLS is the dominant SESSION_CONTEXT use case)
SET @sql = N'USE ' + QUOTENAME(@db) + N';
SELECT DB_NAME(), ps.name, p.name, p.is_enabled, p.is_schema_bound,
QUOTENAME(OBJECT_SCHEMA_NAME(pr.target_object_id)) + N''.'' + QUOTENAME(OBJECT_NAME(pr.target_object_id)),
pr.predicate_type_desc, pr.operation_desc, pr.predicate_definition
FROM sys.security_policies p
JOIN sys.schemas ps ON p.schema_id = ps.schema_id
JOIN sys.security_predicates pr ON p.object_id = pr.object_id;';
INSERT #Policies
EXEC sys.sp_executesql @sql;
END TRY
BEGIN CATCH
PRINT N'Skipped database [' + @db + N']: ' + ERROR_MESSAGE();
END CATCH
FETCH NEXT FROM db_cur INTO @db;
END
CLOSE db_cur;
DEALLOCATE db_cur;
-----------------------------------------------------------------------------------------
-- Plan-cache scan (instance-wide, point-in-time). Catches ad hoc / app SESSION_CONTEXT
-- usage that lives in pooled-connection batches rather than in stored modules.
-----------------------------------------------------------------------------------------
IF @ScanPlanCache = 1
BEGIN
BEGIN TRY
INSERT #PlanCache (DatabaseName, ObjectName, ExecutionCount, LastExecution, QuerySnippet)
SELECT TOP (@PlanCacheTopN)
DB_NAME(st.dbid),
CASE WHEN st.objectid IS NULL THEN N'(ad hoc / prepared)'
ELSE QUOTENAME(OBJECT_SCHEMA_NAME(st.objectid, st.dbid)) + N'.' + QUOTENAME(OBJECT_NAME(st.objectid, st.dbid))
END,
qs.execution_count,
qs.last_execution_time,
SUBSTRING(st.text, 1, 600)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE st.text COLLATE Latin1_General_CI_AI LIKE @PatRead
ORDER BY qs.execution_count DESC;
END TRY
BEGIN CATCH
PRINT N'Plan-cache scan skipped: ' + ERROR_MESSAGE();
END CATCH
END
-----------------------------------------------------------------------------------------
-- Trace flag 11042 state: runtime (global) and startup parameter
-----------------------------------------------------------------------------------------
BEGIN TRY
INSERT #TF EXEC ('DBCC TRACESTATUS(-1) WITH NO_INFOMSGS');
END TRY
BEGIN CATCH
PRINT N'DBCC TRACESTATUS failed: ' + ERROR_MESSAGE();
END CATCH
DECLARE @TF11042Runtime bit = CASE WHEN EXISTS (SELECT 1 FROM #TF WHERE TraceFlag = 11042 AND [Global] = 1) THEN 1 ELSE 0 END;
DECLARE @TF11042Startup bit = 0;
BEGIN TRY
SELECT @TF11042Startup = CASE WHEN EXISTS (
SELECT 1 FROM sys.dm_server_registry
WHERE registry_key LIKE N'%\MSSQLServer\Parameters'
AND CAST(value_data AS nvarchar(4000)) LIKE N'%11042%') THEN 1 ELSE 0 END;
END TRY
BEGIN CATCH
PRINT N'Startup parameter check skipped: ' + ERROR_MESSAGE();
END CATCH
-----------------------------------------------------------------------------------------
-- SUMMARY (read this first)
-----------------------------------------------------------------------------------------
DECLARE @HasRead bit = CASE WHEN EXISTS (SELECT 1 FROM #Modules WHERE UsesReadContext = 1) THEN 1 ELSE 0 END;
DECLARE @HasSet bit = CASE WHEN EXISTS (SELECT 1 FROM #Modules WHERE UsesSetContext = 1) THEN 1 ELSE 0 END;
DECLARE @HasRLS bit = CASE WHEN EXISTS (SELECT 1 FROM #Policies) THEN 1 ELSE 0 END;
DECLARE @HasPlan bit = CASE WHEN EXISTS (SELECT 1 FROM #PlanCache) THEN 1 ELSE 0 END;
DECLARE @InScope bit = CASE WHEN @HasRead = 1 OR @HasRLS = 1 OR @HasPlan = 1 THEN 1 ELSE 0 END;
DECLARE @Mitigated bit = CASE WHEN @TF11042Runtime = 1 AND @TF11042Startup = 1 THEN 1 ELSE 0 END;
-- Compact trace-flag state
DECLARE @TFState varchar(30) =
CASE
WHEN @TF11042Runtime = 1 AND @TF11042Startup = 1 THEN 'Startup + Runtime'
WHEN @TF11042Runtime = 1 AND @TF11042Startup = 0 THEN 'Runtime only'
WHEN @TF11042Runtime = 0 AND @TF11042Startup = 1 THEN 'Startup (pending)'
ELSE 'Off'
END;
-- Definitive status token and one-line action
DECLARE @Status varchar(24) =
CASE
WHEN @InScope = 0 THEN 'PASS'
WHEN @InScope = 1 AND @Mitigated = 1 THEN 'PASS (MITIGATED)'
WHEN @InScope = 1 AND @TF11042Startup = 1 THEN 'PASS (AFTER RESTART)'
ELSE 'ACTION REQUIRED'
END;
DECLARE @Action varchar(200) =
CASE
WHEN @InScope = 0 THEN 'No SESSION_CONTEXT usage. Patch normally.'
WHEN @InScope = 1 AND @Mitigated = 1 THEN 'In scope but TF11042 set at startup and active. Patch normally.'
WHEN @InScope = 1 AND @TF11042Startup = 1 THEN 'In scope. -T11042 set at startup; engages on the patch restart.'
WHEN @InScope = 1 AND @TF11042Runtime = 1 THEN 'In scope. TF11042 runtime-only and lost on restart. Add -T11042 to startup before patching.'
ELSE 'In scope and unmitigated. Add -T11042 to startup (DBCC TRACEON(11042,-1) for immediate effect) before/with the CU.'
END;
SELECT
Instance = CAST(SERVERPROPERTY('ServerName') AS varchar(64)),
Build = CAST(SERVERPROPERTY('ProductVersion') AS varchar(20)) + ' / '
+ CAST(SERVERPROPERTY('ProductUpdateLevel') AS varchar(10)),
[Status] = @Status,
Modules = (SELECT COUNT(*) FROM #Modules WHERE UsesReadContext = 1),
RLS_Policies = (SELECT COUNT(*) FROM #Policies),
PlanCache = (SELECT COUNT(*) FROM #PlanCache),
TF11042 = @TFState,
Action = @Action;
-----------------------------------------------------------------------------------------
-- DETAIL: only emitted when the instance is in scope (keeps a clean instance to one row)
-----------------------------------------------------------------------------------------
IF @InScope = 1
BEGIN
-- Modules referencing SESSION_CONTEXT
SELECT DatabaseName, SchemaName, ObjectName, ObjectType, UsesSetContext, UsesReadContext
FROM #Modules
ORDER BY UsesReadContext DESC, DatabaseName, SchemaName, ObjectName;
-- Row-Level Security policies and predicates (cross-reference predicate functions above)
SELECT DatabaseName, PolicySchema, PolicyName, PolicyEnabled, IsSchemaBound,
TargetObject, PredicateType, Operation, PredicateDefinition
FROM #Policies
ORDER BY DatabaseName, PolicySchema, PolicyName;
-- Plan-cache hits (point-in-time; only populated if @ScanPlanCache = 1)
SELECT DatabaseName, ObjectName, ExecutionCount, LastExecution, QuerySnippet
FROM #PlanCache
ORDER BY ExecutionCount DESC;
-- Trace flag runtime state
SELECT TraceFlag, [Status], [Global], [Session] FROM #TF ORDER BY TraceFlag;
END
-- Cleanup
DROP TABLE #Modules, #Policies, #PlanCache, #TF;
What the output looks like
Run it in SQL Server Management Studio and read the first result set. A clean instance, with no SESSION_CONTEXT usage anywhere, comes back as a single row:
Instance Build Status Modules RLS_Policies PlanCache TF11042 Action
sqlservern 16.0.4245.2 / CU24 PASS 0 0 0 Off No SESSION_CONTEXT usage. Patch normally.
That is the result you want to see: Status is PASS, the counts are all zero, and the Action tells you to patch normally. When Status instead reads ACTION REQUIRED (or one of the PASS (MITIGATED) / PASS (AFTER RESTART) variants), the detail result sets appear beneath the summary and list exactly where SESSION_CONTEXT is used, the modules, the Row-Level Security policies, and any plan-cache hits, so you can confirm the scope and apply trace flag 11042 before you patch.
Key takeaways
- The
SESSION_CONTEXTparallel-plan issue is a long-standing known issue, discovered in January 2022, that Microsoft has carried unfixed through every cumulative update since SQL Server 2019 CU14, including SQL Server 2022 CU25. - The bug fires only when three conditions coincide: a query reads
SESSION_CONTEXT(), the plan goes parallel, and the session has been reset for reuse by connection pooling. - Most estates are not affected, because they do not use the feature. The real risk concentrates in multi-tenant applications that use Row-Level Security with pooled connections and heavier parallel queries.
- The wrong-results variant is more dangerous than the crash, because under RLS it can silently return another tenant's rows.
- The mitigation is trace flag 11042, set as both a startup parameter (
-T11042) and globally (DBCC TRACEON (11042, -1)). It is safe and low-cost. - Check before you patch. If an instance does not use
SESSION_CONTEXT, this issue is no reason to delay the cumulative update.
What to do next
If you run a multi-tenant application on SQL Server, or you are simply not certain whether SESSION_CONTEXT is anywhere in your estate, make the detection script above a standard step in your patch runbook and enable trace flag 11042 on any instance that comes back in scope. It also pays to keep across the broader stream of SQL Server security updates rather than reacting to one known issue at a time. If you would rather have someone confirm your exposure, set the mitigation correctly, and verify your cumulative update strategy across the whole fleet, that is exactly the kind of thing our SQL Server health check covers. DBA Services can review your patch posture, your Row-Level Security configuration, and your trace flag settings, and give you a clear plan before your next maintenance window.
Get a SQL Server Health Check for $999
Find out what's really going on inside your SQL Server environment. We find critical misconfigurations in 97% of reviews, with a full 48-hour performance baseline and a prioritised action plan.
$999 ex-GST
normally $2,499
For your first SQL Server instance; additional instances are 2 hours each.