SOS_SCHEDULER_YIELD Wait Stats: Reading CPU Pressure

SOS_SCHEDULER_YIELD wait stats count the moments a query gives up the CPU after its turn and waits for it again. A little of it is normal on any busy server. A lot of it, with a line of tasks behind it, means your queries want more CPU than you have.

The cooks keep trading burners in a square dance called by Quinn, and Casey finds Jesse searching a towering stack of cheese slices with tweezers for four slices. Casey explains, "Not too few burners. Too much work on each."

This post is part of my wait stats series, told as one story at the Clipboard Diner. Every post is listed in the series guide.

Night 8 at the Clipboard Diner

After the house rules worked on Tuesday, Casey expected a quiet Wednesday. The kitchen has one more rule, older than the door sign. Each cook gets a short turn at a burner, timed by a little sand timer. When the sand runs out, the cook glances at the rail.

If a ticket is waiting, the cook steps back and lets it in. If the rail is empty, the cook flips the timer and keeps going. It’s a fair rule. Nobody hogs a burner while a hungry trucker waits.

Tonight the rule turned the line into a square dance. Ace stepped in, Kit stepped out, Jesse stepped in, and Jules stepped out. The sand timers never stopped flipping. Casey checked every ticket on the rail. Nobody was waiting for potatoes, and nothing needed the basement. Every ticket had all it needed except a burner.

Then Casey watched Jesse work a grilled cheese. The sandwich needed four slices of cheese. To find them, Jesse flipped through the whole tray at the burner, all two hundred slices. Kit was doing the same with the tomatoes. The burners weren’t too few. The work on them was too big.

The clipboard line that night was plain: Nobody waited for potatoes. Everybody waited for a burner.

What SOS_SCHEDULER_YIELD Means

That sand timer is how SQL Server shares a CPU. Each scheduler runs one worker at a time, and workers take turns by agreement. A worker runs until it has to wait for something, or until its turn, called a quantum, runs out. The quantum is 4 milliseconds.

When the quantum runs out, the worker yields on its own. If other tasks are waiting for that scheduler, it goes to the back of the line. The time it spends in that line is recorded as SOS_SCHEDULER_YIELD.

This wait needs no resource. The task isn’t waiting for a page, a lock or memory. It only waits for the CPU, so almost all of its wait time is signal wait. If that term is new, read Signal Wait Stats first.

One detail makes this wait easy to misread. When nobody else is waiting, the worker yields and gets the CPU back at once. The wait still counts, but with almost no time. So a huge count with a tiny average means busy CPUs and no line. That’s Jesse flipping the timer with an empty rail.

SOS_SCHEDULER_YIELD, what it is: Heavy query uses full turns, then 4 ms turn runs out, then back of the line, then gets a cpu again. The time is lost at "Back of the line". Normal: High count, average near 0 ms, no line; Watch: CPU jumps after a release or change; Act: High wait time, runnable tasks above 0.

Normal or a Problem?

SituationWhat it meansWhat to do
High count, average wait near 0 ms, runnable tasks near 0Busy CPUs, no line.Normal. Leave it.
High total wait time and runnable tasks above 0 for long stretchesTasks queue for CPU.Find the top CPU queries below.
CPU jumped after a release or a plan changeA plan got worse.Compare plans in Query Store and force the good one.
It rides along with heavy parallel waitsToo many threads compete for the same CPUs.Check Parallelism Wait Stats.

See It on Your Server

The first query reads the yield wait and the line at each scheduler. Run it during your busiest hour, a few times in a row.

-- The yield wait since the last restart
SELECT wait_type,
       waiting_tasks_count,
       wait_time_ms,
       signal_wait_time_ms,
       CAST(1.0 * wait_time_ms / NULLIF(waiting_tasks_count, 0) AS decimal(18, 2)) AS avg_wait_ms
FROM sys.dm_os_wait_stats
WHERE wait_type = N'SOS_SCHEDULER_YIELD';

-- The line of tasks at each scheduler right now
SELECT scheduler_id,
       current_tasks_count,
       runnable_tasks_count,
       active_workers_count,
       work_queue_count
FROM sys.dm_os_schedulers
WHERE status = N'VISIBLE ONLINE';

Look at avg_wait_ms and runnable_tasks_count together. A tiny average with no line is a busy kitchen doing fine. A steady line on most schedulers is real CPU pressure, and the queries below are where it comes from.

The second query lists the statements that used the most CPU since their plans were cached. CPU time in this view is in microseconds, so the query divides by 1,000.

-- Which statements used the most CPU since their plans were cached?
SELECT TOP (10)
       qs.total_worker_time / 1000 AS cpu_ms_total,
       qs.execution_count AS runs,
       CAST(qs.total_worker_time / 1000.0 / NULLIF(qs.execution_count, 0) AS decimal(18, 2)) AS cpu_ms_per_run,
       qs.total_logical_reads AS pages_read,
       st.statement_text
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS t
CROSS APPLY (SELECT SUBSTRING(t.text, qs.statement_start_offset / 2 + 1,
                 (IIF(qs.statement_end_offset = -1, DATALENGTH(t.text), qs.statement_end_offset)
                  - qs.statement_start_offset) / 2 + 1) AS statement_text) AS st
ORDER BY qs.total_worker_time DESC;

Read cpu_ms_total and runs together. One heavy query and one tiny query that runs a million times can both top this list. Check pages_read too. It counts pages read from memory, and a statement that reads millions of them is Jesse flipping through the cheese.

How to fix SOS_SCHEDULER_YIELD, in order: 1. Confirm the line is real; 2. Tune top CPU statements, add indexes; 3. Fix plan regressions in Query Store; 4. Check the parallelism settings; 5. Check power plan and VM CPU; 6. Add CPUs last, after tuning. Check first: Average wait and runnable tasks.

Fix It

  1. Confirm the line is real. Measure a busy window and watch runnable tasks, as in Wait Stats Over Time.
  2. Tune the top three CPU statements. Add indexes that turn scans into seeks, and remove implicit conversions.
  3. Look for plan regressions in Query Store, and force the last good plan while you fix the cause.
  4. Check the parallelism settings, so small queries stop grabbing extra threads.
  5. Check the Windows power plan and the VM’s CPU allocation. A slowed CPU looks like a busy one.
  6. Add CPUs last, after the queries are tuned.

You could say more CPU is cheaper than a week of tuning. Sometimes it is. But most SQL Server licenses are sold per core. Then every new core costs twice: once for the hardware and once for the license.

Buying cores before anyone reads the queries is the expensive version of this mistake. The new server feels fast for a month. Then the same few queries fill it up again, and you tune them anyway.

New in SQL Server 2022 and 2025

Nothing changed in what this wait means. The quantum is still 4 ms, and the line still forms the same way. What changed is how easy it is to find the cause.

Since SQL Server 2022, Query Store is on by default for new databases. CPU history per query is there when you need it. Parameter Sensitive Plan optimization arrived at compatibility level 160. It keeps more than one plan when the best plan depends on the parameter. That cuts some CPU spikes from bad plan reuse. In SQL Server 2025 Enterprise and Enterprise Developer editions, DOP feedback is on by default and trims wasted parallel threads. It needs compatibility level 160 or higher and Query Store in READ_WRITE mode.

Related Reading

The Clipboard Diner, a wait stats series. Previous: Parallelism Wait Stats: Tuning MAXDOP and Cost Threshold. Next: PAGEIOLATCH Wait Stats: Waiting for Data From Disk. Every post is listed in the series guide.

Next, the prep counter runs out of room, and the basement stairs get busy.

SOS_SCHEDULER_YIELD is not a call to buy CPUs, it is a call to read your top CPU queries.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL CPU, SQL DMV, SQL Scripts, SQL Wait Stats
Previous Post
Parallelism Wait Stats: Tuning MAXDOP and Cost Threshold
Next Post
PAGEIOLATCH Wait Stats: Waiting for Data From Disk

Related Posts

17 Comments. Leave new

  • I’ve often observed SOS_SCHEDULER_YIELD as “last_wait_type” instead of “wait_type”, where a unique session is consuming 100% of 1 cpu for hours (due to stale stats incurring nested loops between huge tables).
    In this situation (no IO, lock, latch, network nor any other waits), SOS_SCHEDULER_YIELD is the only event waited on, which means… no wait all all, am I right ?

    Reply
  • stanleyjohns
    July 14, 2011 1:32 am

    I had this wait recently on one of our systems. Clients were reporting slow responses from the server. Doing a DBCC freeproccache and a DBCC freesystemcache fixed the issue.

    Reply
    • You basically cleared the cache from your system. Most likely there must be a bad execution plan which occasionally works good for few values out of the sproc, could also be issue of parameter sniffing.

      Reply
  • Bhavisha Patel
    April 15, 2012 7:57 am

    I have same issue in production while running SSI sin for each loop which query million of rows. I am seeing this king of wait in “last wait type” session is showing Insert as running ststement but count in table is 0. As soon i as I kill session everthing start working as it should….Please suggest.

    Reply
  • I have situation:
    SP work good from query, but not work from Excel (sos_scheduler_yield).

    I FOUND RESOLUTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    –> Just ReCreate this SP.

    But, I can’t explain this? Pinal?

    Reply
  • I had the same situation and just fixed the issue by dropping and recreating the sp. what was behind ?

    Reply
  • I had the same issue, and fixed by drop and create the sp
    how this can be explained ?

    Reply
    • Instead of dropping the procedure or blowing away the cache, you could first try exec sp_recompile [storedProcedureName]; to recompile the execution plan. Sounds like a potential parameter sniffing matter on your proc or change in statistics for your table when a better plan could be reached. Overall, sounds like a bad execution plan.

      Reply
  • prince rastogi
    March 20, 2013 9:28 pm

    Excessive CPU use may be the reason of Spinning and backoff..

    Reply
  • Alankar Chakravorty
    March 22, 2015 2:26 pm

    Is there a way to find out on which database was the expensive query executed on when 1 SQL Server instance is serving multiple user databases?

    Reply
    • have you looked into sys.dm_exec_query_stats

      Reply
      • Alankar Chakravorty
        March 30, 2015 12:15 am

        Yes. I think the only way to pull out the database name on which the query has been execute is to do an inner join with sys.dm_exec_requests on session_id. But then it will only return the data when the SQL statement is executing.

        Not sure how to relate the high intensive queries with the databases when the queries have already completed execution.

        Until unless we know the database for which the queries have been executed, those heavy queries cannot be worked upon or further tuned to reduce the CPU utilization.

  • hello ,
    whenever i run select * from sys.sysprocessess for the same spid which i am running this query is having lastwait type as ASYNC_NETWORK_IO , is this a problem please suggest
    thanks in advance

    Reply
    • You have to look at wait time along with wait type. I am guessing that this query is fetching large number of rows so it would be normal.

      Reply
  • Dear Pinal,

    Have a question on Scheduler. In my case first 4 schedulers are having runnable task(sometimes double digit),But last 4 scheduler always be 0
    Why? Does it mean only 4 schedulers are being used? and rest are idle.

    Reply

Leave a Reply

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

Fill out this field
Fill out this field
Please enter a valid email address.