SQL Server CPU Utilization History Report
SQL Server records CPU utilization data internally every minute and stores the last 256 readings in a ring buffer. This T-SQL script reads that buffer to produce a CPU history report showing SQL Server CPU usage, other process CPU usage, and total server CPU load — without requiring any external monitoring tool.
Purpose and Overview
When a CPU spike occurs on a SQL Server host, the first question is whether SQL Server caused it or whether another process did. The sys.dm_os_ring_buffers DMV stores CPU statistics that SQL Server collects from the Windows performance counters every 60 seconds, retaining the last 256 minutes of data.
This script parses the XML stored in the ring buffer and returns one row per minute with three CPU columns:
- sql_cpu_utilization — percentage of total CPU consumed by the SQL Server process
- other_cpu_utilization — percentage consumed by all other processes on the host
- total_cpu_utilization — combined SQL Server plus other processes (100 minus the idle percentage)
Use this report after an incident to reconstruct what happened, or run it proactively to detect trending CPU pressure before it becomes a problem.
Code Breakdown
1DECLARE @ts_now BIGINT;
2
3SELECT @ts_now = cpu_ticks / (cpu_ticks / ms_ticks)
4FROM sys.dm_os_sys_info;
5
6SELECT TOP 256
7 DATEADD(
8 ms,
9 -1 * (@ts_now - [timestamp]),
10 GETDATE()
11 ) AS event_time,
12 xmldata.value(
13 '(Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]',
14 'int'
15 ) AS sql_cpu_utilization,
16 xmldata.value(
17 '(Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]',
18 'int'
19 ) AS system_idle,
20 100
21 - xmldata.value(
22 '(Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]',
23 'int'
24 )
25 - xmldata.value(
26 '(Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]',
27 'int'
28 ) AS other_cpu_utilization,
29 100
30 - xmldata.value(
31 '(Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]',
32 'int'
33 ) AS total_cpu_utilization
34FROM (
35 SELECT
36 [timestamp],
37 CONVERT(XML, record) AS xmldata
38 FROM sys.dm_os_ring_buffers
39 WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR'
40 AND record LIKE '%<SystemHealth>%'
41) AS ring
42ORDER BY
43 [timestamp] DESC;
Reading the Ring Buffer
sys.dm_os_ring_buffers stores diagnostic data that SQL Server collects for internal monitoring. The ring_buffer_type column identifies what kind of data each row contains. Filtering to RING_BUFFER_SCHEDULER_MONITOR selects the scheduler monitor records, which include CPU statistics. Each row's record column contains an XML document; the subquery converts that raw text to typed XML so the outer query can extract values using .value().
Calculating the Timestamp
The ring buffer stores a timestamp column in CPU tick units, not wall-clock time. The script converts ticks to milliseconds using the ratio from sys.dm_os_sys_info:
1@ts_now = cpu_ticks / (cpu_ticks / ms_ticks)
@ts_now represents "now" in milliseconds. For each ring buffer row, (@ts_now - [timestamp]) gives the number of milliseconds in the past that row was recorded. DATEADD(ms, -1 * offset, GETDATE()) converts that back to a readable datetime.
Extracting CPU Values from XML
Each XML record contains three values used by this script:
ProcessUtilization— CPU percentage used by the SQL Server process (sqlservr.exe)SystemIdle— CPU percentage that was idleother_cpu_utilizationis derived:100 - SystemIdle - ProcessUtilizationtotal_cpu_utilizationis derived:100 - SystemIdle
The .value() XQuery function extracts each field by XPath and casts it to int.
Filtering Valid Records
The WHERE record LIKE '%<SystemHealth>%' predicate ensures only scheduler monitor health records are included. Other record types share the same ring_buffer_type and would cause XML parsing errors if included.
Result Order
ORDER BY [timestamp] DESC returns the most recent minute first. The ring buffer never holds more than 256 rows of this type, so TOP 256 is the natural limit.
Key Benefits and Use Cases
- Post-incident analysis — reconstruct a CPU spike from up to four hours ago without needing external monitoring data
- Blame attribution — determine whether high CPU was caused by SQL Server or another process on the host
- Trend detection — spot gradual CPU growth that may indicate a query plan regression or data growth problem
- No agent required — reads live in-memory data; no SQL Agent job or third-party tool needed
- Lightweight — reads only DMV memory structures with no disk I/O or table scans
Performance Considerations
- Data is lost on restart — the ring buffer is in-memory only. A SQL Server restart or failover clears all history. If an incident caused a crash, this data will not be available.
- 256-minute limit — the ring buffer holds exactly 256 one-minute samples. Events older than roughly four hours are overwritten.
- 1-minute granularity — the report cannot show sub-minute CPU spikes. For sub-second precision, use Performance Monitor or Extended Events.
- Tick arithmetic on busy systems — on systems with very high CPU tick rates, the tick-to-millisecond conversion is an approximation. Event times are accurate to within a few seconds.
- SQL Server process only — this report shows the aggregate CPU for the entire SQL Server process. It does not break down CPU by individual query or session. Use
sys.dm_exec_query_statsor Query Store for per-query CPU.
Practical Tips
- Compare against
sys.dm_exec_requests— ifsql_cpu_utilizationwas high at a specific minute, cross-reference with job history or Query Store to identify which queries were running then. - Set a threshold alert — wrap this query in a SQL Agent job that sends an alert if
sql_cpu_utilizationexceeds 80 for three consecutive minutes. - Capture to a history table — insert results into a permanent table on a schedule to build longer-term CPU trend history beyond the 256-minute window.
- Check
other_cpu_utilization— if this value is consistently high, investigate non-SQL processes on the host such as antivirus scans, backup agents, or ETL tools. - Pair with wait statistics — high SQL Server CPU combined with high
SOS_SCHEDULER_YIELDwait type is a strong indicator of CPU pressure from query execution.
Conclusion
The sys.dm_os_ring_buffers CPU history script gives you an immediate, tool-free view of CPU utilization over the past four hours. It clearly separates SQL Server CPU from other process CPU, making it straightforward to determine the source of a spike. Add it to your incident response toolkit alongside wait statistics and query performance queries for a complete picture of server health.
References
- sys.dm_os_ring_buffers (Microsoft Docs) — DMV containing internal diagnostic data including CPU history collected by the scheduler monitor
- sys.dm_os_sys_info (Microsoft Docs) — DMV providing CPU tick and millisecond values used to convert ring buffer timestamps
- Glenn Berry's SQL Server Diagnostic Queries — Widely used DBA diagnostic scripts including CPU and ring buffer queries
- ktaranov/sqlserver-kit on GitHub — Community collection of SQL Server DBA and performance scripts