How To Get Per-Core CPU Usage

It’s pretty easy to get the average CPU usage, but how about calculating the per-code load on multicore systems? Turns out it’s also simple enough if you use performance counters.

Windows API includes a subset of functions that provide various performance-related information, which includes data on how busy individual processors or cores are. In this post I’ll show you how to retrieve and use this information. I’ve also included a simple example application that demonstrates the functionality (the download link is at the end of the post) :

Screenshot of the example application

Screenshot of the example application

The example app was written in Delphi, but the salient parts are almost purely Windows API so they should be easy to translate to other programming languages.

Algorithm Overview

Here’s a general overview of how to use the performance counter API to get the per-core usage numbers.


  1. Create a performance query using PdhOpenQuery.
  2. Generate a list of performance counter paths (one for each CPU or core) by feeding a wildcard path to PdhExpandWildCardPath. In this case, this is the path that we need :
    \Processor(*)\% Processor Time
    You can read more about counter path syntax here.

    Note : Unfortunately, the API makes no distinction between multiple physical processors and multiple cores, so you will get a “…\Processor(X)\…” entry for each core.

  3. Add the generated counter paths to the query using PdhAddCounter and save the returned handle(s) for later.

Collecting & Displaying Data
CPU usage is typically measured over an interval of time, so you will need to collect at least two data samples at different times to get a meaningful result. Depending on your needs, you could either :

  • Collect one sample, sleep() for a second, collect another sample and calculate the result; OR
  • Set up a timer or a loop to calculate & displays the CPU usage periodically. That’s what I’ll do in this tutorial.

Regardless of whether you only need a one-time measurement or periodic updates, the process is very similar API-wise. First, call PdhCollectQueryData to update all counters associated with the query. Then call PdhGetFormattedCounterValue with each counter handle to get the actual data. If you set the output format flag to PDH_FMT_DOUBLE the function will give you the CPU/core load percentage as a floating-point value in the range of [0..100].

If you need periodic updates just call these two functions repeatedly.

When you’re done, call PdhCloseQuery to close counters associated with the query and free up allocated system resources. You don’t need to explicitly remove individual counters.


This is the source code of a simple Delphi 2009 application that displays the load percentage of each processor core in a progress bar. The output is updated every second by using a timer component.

unit CoreUsage;


  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, JwaWindows, ExtCtrls, ComCtrls, Gauges;

  TForm1 = class(TForm)
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    { Private declarations }
    { Public declarations }

  TCounter = record
   Path : string;
   Handle : cardinal;
   mLabel : TLabel;
   mGauge : TGauge;

  Form1: TForm1;
  Counters : array of TCounter;
  Query : Cardinal = 0;


{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
 dwSize, h : cardinal;
 cnt, i : Integer;
 InstanceId, CounterPath : string;
 status : PDH_STATUS;
  //Create a performance query
  if  PdhOpenQuery(nil, 0, Query) = ERROR_SUCCESS then begin

    //Get all valid processor/core usage counter paths by expanding
    //a wildcard path. To do this, we must first call PdhExpandWildCardPath
    //with buffer size set to zero to get the actual required buffer size.
    dwSize := 0;
    pPaths := nil;

    status := PdhExpandWildCardPath(
      nil,                                  //search the local computer
      '\Processor(*)\% Processor Time', //we want CPU usage counters for all CPUs/cores
      pPaths,                               //user-allocated buffer; currently null
      dwSize,                               //buffer size
      0) ;                                  //no flags

    if status = PDH_MORE_DATA then begin
      dwSize := dwSize + 1; //+1 byte required in XP and below.
      pPaths := GetMemory(dwSize); //Allocate an output buffer.

      //Really get the counter paths.
      status := PdhExpandWildCardPath(
        '\Processor(*)\% Processor Time',
        0) ;

      if status = ERROR_SUCCESS then begin

        cnt := 0;
        SetLength(Counters, 32);

        //PdhExpandWildCardPath returns the counter list (pPaths) as an array
        //of null-terminated strings where the last item is a zero-length
        //string (i.e. just #0). We'll now iterate over this list with some
        //simple pointer math.
        pIterator := pPaths;
        while (strlen(pIterator)>0) do begin

          CounterPath := pIterator;
          pIterator := pIterator + Length(pIterator) + 1;

          //Find the counter instance ID (the part in parentheses)
          i := Pos('(', CounterPath);
          InstanceId := Copy(CounterPath, i+1, Pos(')', CounterPath)-i-1);
          //Skip the counter if it indicates the overall CPU usage,
          //we only want the per-core values.
          if InstanceId = '_Total' then continue;

          //Add the counter to the query
          status := PdhAddCounter(Query, PWideChar(CounterPath), 0, h);

          if status = ERROR_SUCCESS then begin

            //Expand the internal counter array if necessary
            if cnt > Length(Counters)-1 then
              SetLength(Counters, Length(Counters)+16);

            //Save the counter data to the array
            with Counters[cnt] do begin
              Path := CounterPath;
              Handle := h;

              //Create a label for this core/CPU
              mLabel := TLabel.Create(Self);
              mLabel.Parent := Self;
              mLabel.Caption := 'Core '+InstanceId;
              mLabel.Top := (mLabel.Height + 8) * cnt + 10;
              mLabel.Left := 10;
              mLabel.AutoSize := false;
              mLabel.Width := 80;

              //Create a "progress bar" to show the core/CPU usage
              mGauge := TGauge.Create(Self);
              mGauge.Parent := Self;
              mGauge.Top := mLabel.Top;
              mGauge.Left := mLabel.Width + 20;
              mGauge.Height := mLabel.Height+2;
              mGauge.Width := Self.ClientWidth - mGauge.Left - 10;
              mGauge.ForeColor := $0031D329;
              mGauge.Anchors := [akRight, akTop];


        //Truncate the array to the actual number of discovered cores  
        SetLength(Counters, cnt);
        //Collect the first data sample


  //Did we get any valid counters?
  if Length(Counters) = 0 then begin

    //Nope. Show an unhelpful error message...
    MessageBox(Handle, 'Initialization was unsuccessful. The application will now exit.',
      'Error', MB_OK or MB_ICONEXCLAMATION);

    //...and quit.

  end else begin
    //Everything went well.
    //Resize the form to make sure all progress bars are visible.
    Self.ClientHeight :=
      Counters[Length(Counters)-1].mLabel.Top +
      Counters[Length(Counters)-1].mLabel.Height +


procedure TForm1.Timer1Timer(Sender: TObject);
 counterType: PDword;
 status : cardinal;
  if Query = 0 then exit;

  //Collect a data sample.

  //Iterate over all counters and update progress bars.
  for i := 0 to Length(Counters) - 1 do begin
    counterType := nil;

    //Get the current core/CPU usage
    status := PdhGetFormattedCounterValue(
      Counters[i].Handle, //Counter handle as returned by PdhAddCounter.
      PDH_FMT_DOUBLE,     //Get the counter value as a double-precision float.
      CounterType,        //Counter type; unused.
      pValue);            //Output buffer for the counter value.

    //Update the progress bar.
    if status = ERROR_SUCCESS then
      Counters[i].mGauge.Progress := Round(pValue.doubleValue);

procedure TForm1.FormDestroy(Sender: TObject);
 //Close the query when quitting.
 if Query <> 0 then PdhCloseQuery(Query);



Rumor has it that you may need administrative rights to use the performance counter API.

Related posts :

41 Responses to “How To Get Per-Core CPU Usage”

  1. Jānis Elsts says:

    It looks like the counter paths have changed a bit in the 8 years since this post was written. Try replacing all instances of \Processor(*/*#*)\% Processor Time with this:
    \Processor(*)\% Processor Time

    I’ll upload a new version of the .exe that I just tested on Windows 10.

Leave a Reply