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. White Shadow says:

    No idea. Are you getting the error at compile time or runtime?

  2. Walian says:


    MessageBoxA(HWND_DESKTOP, lpError, ‘Error:’, MB_OK);

  3. Anonymous says:

    […] gefunden der eine ganz nette Sache beschreibt. Und zwar das Auslesen der CPU Last pro Core. Funktioniert super – solange man ein englisches System nutzt. Denn die Performance Counter werden […]

  4. I’ve downloaded the compiled demo application and it fails to initialize on Windows 7 Ultimate 64 bit.

  5. White Shadow says:

    Considering that it was compiled on Windows XP 32bit, I’m not the least bit surprised. The general algorithm should still be valid for Windows 7, though.

  6. yes, BUT still, windows 7 x64 is capable of running 32 bit applications… I have tested it in a VM with Windows Xp sp 3 32 bit version running and it works perfectly…

  7. White Shadow says:

    Unfortunately, I don’t have Delphi installed right now, so I can’t check what goes wrong in Windows 7.

  8. Chris says:

    Actually the problem is here

    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

    the value of status is zero.
    Any ideas why?

  9. White Shadow says:

    According to MSDN, zero equals ERROR_SUCCESS. And according to another page on MSDN, that means the function executed successfully, i.e. no error.

  10. RASOLOFONIAINA Menjanahary R. says:

    Was the Windows 7 x64 fixed?

  11. RASOLOFONIAINA Menjanahary R. says:

    I mean the windows 7 x64 issue.

  12. Jānis Elsts says:

    No, I don’t think so.

  13. Josh says:

    Does not work on Windows 7 x64. Can you update the program so that it works?

  14. Jānis Elsts says:

    Unfortunately not. It appears regardless of what wildcard path the application tries, PdhExpandWildcardPath always returns an “invalid path” error on Win 7 x64. I don’t know why that is, and I don’t really have the time to investigate. Sorry.

  15. Johan Nilsson says:

    Replace both instances of: ‘\Processor(*/*#*)\% Processor Time’
    with: ‘\Processor(*)\% Processor Time’

    Worked on Windows7 Ultimate x64 with Delphi2010

  16. Johan Nilsson says:

    Also, if you don’t care to install the Jedi API library do this:

    1. Remove JwaWindows from the uses-clause

    2. Include the following in the interface section of the Form-unit

      PDH_HQUERY   = THandle;
      PDH_HCOUNTER = THandle;
      _PDH_FMT_COUNTERVALUE = record
        CStatus: DWORD;
        case Longint of
          1: (longValue: Longint);
          2: (doubleValue: Double);
          3: (largeValue: LONGLONG);
          4: (AnsiStringValue: LPSTR);
          5: (WideStringValue: LPCWSTR);
      function PdhAddCounter(hQuery: PDH_HQUERY; szFullCounterPath: LPCTSTR; dwUserData: DWORD_PTR; var phCounter: PDH_HCOUNTER): PDH_STATUS; stdcall;
      function PdhCloseQuery(hQuery: PDH_HQUERY): PDH_STATUS; stdcall;
      function PdhCollectQueryData(hQuery: PDH_HQUERY): PDH_STATUS; stdcall;
      function PdhGetFormattedCounterValue(hCounter: PDH_HCOUNTER; dwFormat: DWORD; lpdwType: LPDWORD; var pValue: PDH_FMT_COUNTERVALUE): PDH_STATUS; stdcall;
      function PdhExpandWildCardPath(szDataSource, szWildCardPath: LPCTSTR; mszExpandedPathList: LPTSTR; var pcchPathListLength: DWORD; dwFlags: DWORD): PDH_STATUS; stdcall;
      function PdhOpenQuery(szDataSource: LPCTSTR; dwUserData: DWORD_PTR; var phQuery: PDH_HQUERY): PDH_STATUS; stdcall;
      PDH_LIB          = 'pdh.dll';
      PDH_FMT_DOUBLE   = DWORD($00000200);
      PDH_MORE_DATA    = DWORD($800007D2);

    3. Include the following in the implementation section of the Form-unit

    function PdhAddCounter; external PDH_LIB name 'PdhAddCounterW';
    function PdhCloseQuery; external PDH_LIB name 'PdhCloseQuery';
    function PdhCollectQueryData; external PDH_LIB name 'PdhCollectQueryData';
    function PdhGetFormattedCounterValue; external PDH_LIB name 'PdhGetFormattedCounterValue';
    function PdhExpandWildCardPath; external PDH_LIB name 'PdhExpandWildCardPathW';
    function PdhOpenQuery; external PDH_LIB name 'PdhOpenQueryW';
  17. Johan Nilsson says:

    All indentation was stripped when submitting the code above as a comment. Sorry for that.

  18. Jānis Elsts says:

    I fixed the indentation and added syntax highlighting.

  19. Alonso says:


    Based upon this great source code, I tried to create a custom plugin for Windows Server 2008 German edition, but the demo is stuck on ‘//Did we get any valid counters?’ coz the result is 0 and the compiled exe too on Windows 10 English version.
    Obviously I don’t have any idea 🙁

Leave a Reply