CXPACKET and CXCONSUMER Wait Types in SQL Server

⏳Part of the SQL Server Wait Types Library, every wait type explained.Related pillar: Performance & Troubleshooting

CXPACKET and CXCONSUMER appear whenever SQL Server runs a query in parallel. They are coordination overhead between worker threads, not waits on a resource.

  • CXPACKET: a producer thread waiting for a consumer to take rows, or the coordinator waiting for all workers to finish.
  • CXCONSUMER: a consumer thread waiting for rows from a producer. Before 2016 SP2 this was counted as CXPACKET.
ℹ️On SQL Server 2019 and later there are five of these waits, and CXPACKET is usually the smallest. If your top wait is CXSYNC_PORT or CXSYNC_CONSUMER, this page still applies: same causes, same fixes.Measured on a 2025 instance: CXCONSUMER 1,139,679 ms, CXSYNC_PORT 427,414 ms, CXSYNC_CONSUMER 13,171 ms, CXPACKET 4,810 ms.

Is This Wait Expected?

Yes, they will appear on any server that runs parallel queries. Some level of CXPACKET/CXCONSUMER is completely normal and indicates SQL Server is using parallelism productively.

It becomes a signal worth investigating when:

  • CXPACKET is your #1 wait type by a wide margin, especially on an OLTP server
  • OLTP queries that should be fast (< 100ms) are running in parallel unnecessarily
  • You’re seeing high avg_wait_ms for CXPACKET (indicates threads are seriously out of sync)
  • Users complain about slow ad-hoc queries that aren’t returning large data sets

When To Ignore It

Analytics and reporting workloads, parallel queries are beneficial for large aggregations, sorts, and scans. CXPACKET being high on a reporting server is expected and healthy.

Nightly batch jobs, ETL, index rebuilds, statistics updates, and DBCC CHECKDB all benefit from parallelism. Ignore CXPACKET during maintenance windows.

Any server with moderate CXPACKET alongside other waits, if CXPACKET is 10 to 15% of total waits and other waits are present, it’s just background parallelism coordination. Only chase it when it’s dominant.


Root Causes

Four causes produce the same exchange waits, and three of them are settings rather than queries. Work out which is yours before changing anything, because two of these fixes are instance-wide.

Cost Threshold for Parallelism Left at 5COMMON

The default is 5, and it has been 5 since 1998. A query is measured in optimiser cost units and anything estimated above the threshold is allowed a parallel plan; on modern hardware trivial OLTP queries clear 5 easily. Raising it to somewhere between 25 and 50 stops short queries going parallel without touching the large analytical ones that should.

How you tell

sys.configurations shows cost threshold for parallelism at 5, and queries you would call fast are showing a non-zero max_dop in sys.dm_exec_query_stats. Confirmed as the shipping default on a 2025 instance.

MAXDOP Set Too High, or Left at 0COMMON

At 0 a query may use every logical processor, up to 64. Each parallel query then spawns threads across all of them, and more threads means more exchange coordination, which is what these waits measure. A common starting point is 4 to 8 for OLTP, or the number of logical cores in one NUMA node, whichever is smaller.

How you tell

max degree of parallelism reads 0, or matches the core count, in sys.configurations. Check it against the NUMA layout rather than the total core count, because the per-node number is the one that matters.

Missing Indexes Turning Seeks Into Parallel ScansCOMMON

A query that should be a narrow index seek falls back to scanning the table when the index is not there, or when the optimiser underestimates how selective the predicate is. A scan that large is costed high enough to go parallel, so the exchange waits are a symptom of the missing index rather than of parallelism itself.

How you tell

The actual plan shows a parallel table or clustered index scan where you expected a seek, and the missing index DMVs suggest an index on the same predicate. Fixing the index removes the parallelism as a side effect.

Skewed Parallelism, One Slow ThreadLESS COMMON

A parallel plan divides rows across threads. When that split is uneven, and one thread receives most of the rows, the others finish and then sit waiting for it. The wait is real but the cause is the distribution, usually stale statistics or genuinely lopsided data.

How you tell

The peak wait is far above the average: on the instance used for this page CXPACKET showed max_wait_time_ms of 349 against 4,810 ms spread over 486 waiting tasks, about ten milliseconds each. A single wait 35 times the average is threads finishing at very different times.


How To Diagnose It

Check current MAXDOP and cost threshold settings (Get MAXDOP Configuration reads both, alongside the NUMA layout they should be set against):

SELECT name, value_in_use, description
FROM sys.configurations
WHERE name IN (
    'max degree of parallelism',
    'cost threshold for parallelism'
);
SSMS results for the sys.configurations query showing cost threshold for parallelism at 5 and max degree of parallelism at 8

Check if specific queries are driving CXPACKET:

SELECT TOP 20
    qs.total_elapsed_time / qs.execution_count  AS avg_elapsed_us,
    qs.max_dop,
    qs.execution_count,
    SUBSTRING(qt.text, (qs.statement_start_offset / 2) + 1,
        ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
          ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) + 1) AS statement_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.max_dop > 1
ORDER BY qs.total_elapsed_time DESC;

Queries with high max_dop that are supposed to be fast OLTP queries are your targets.

Spot skewed parallelism: look for queries where the actual execution plan shows very uneven row counts across parallel threads. In SSMS: run the query with “Include Actual Execution Plan”, then look at the parallel operators, hover over the arrows to see per-thread row counts.

This is what a parallel plan looks like. The Parallelism operators are where the work is split across threads and gathered back, and on the scan, Number of Executions reads 4: one per thread, matching MAXDOP 4. Those hand-offs between threads are where CXPACKET and CXCONSUMER waits come from.

SSMS actual execution plan for a GROUP BY over 1,000,000 rows with OPTION (MAXDOP 4): Clustered Index Scan, Hash Match partial aggregate, Parallelism Repartition Streams, Sort, Stream Aggregate and Parallelism Gather Streams. The scan tooltip shows Number of Executions 4 and Actual Number of Rows for All Executions 1000000.

What To Do

In priority order. The first two are instance-wide settings, so they change every query on the server; the rest get narrower as you go down.


STEP1

Step 1: Raise the Cost Threshold for Parallelism

Almost always the first fix, and the cheapest: it stops small queries going parallel at all rather than changing how the big ones behave.

EXEC sp_configure 'cost threshold for parallelism', 50;
RECONFIGURE;

Start at 25 and watch, rather than jumping to 50 and hoping. The right value depends on what your workload actually costs; MAXDOP and Cost Threshold covers how to pick it and why 5 was never a recommendation.


STEP2

Step 2: Set MAXDOP to Something Deliberate

This caps how many processors any single query may use. It does not stop parallelism, it stops one query taking the whole box.

EXEC sp_configure 'max degree of parallelism', 4;
RECONFIGURE;

For mixed OLTP and reporting, 4 to 8 is a common starting point. On a NUMA system use the number of logical cores in one node, because a query spanning nodes pays for memory access as well as coordination.


STEP3

Step 3: Fix the Query, Not the Instance

The two settings above are instance-wide, so they change every query on the server. When one query is the problem, hint that query instead while you work out why it goes parallel.

-- illustrative shape, not runnable
SELECT ... FROM ... WHERE ... OPTION (MAXDOP 1);

Treat the hint as a splint, not a fix. If the plan went parallel because of a scan, the real answer is the index, and Get Missing Indexes will usually name it.


STEP4

Step 4: Address Skewed Parallelism

If the threads are finishing at very different times, the split is uneven and no setting will fix it. Update the statistics on the tables involved, add the index that avoids the large scan, and look at whether the data itself is lopsided enough to need a filtered index.


STEP5

Step 5: Separate the Workloads With Resource Governor

Enterprise edition only, and last on the list because it manages the problem rather than removing it. A different MAX_DOP per workload group lets reporting queries use more processors while OLTP traffic is held down, which is the right shape when both genuinely run on one instance.


Related Scripts


💾Get the script: Get-WaitStatistics.sql, the wait breakdown this page reads.PowerShell wrapper: Get-WaitStatistics.ps1

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *