APM Tips

Application Insights

Performance counters and runtime metrics in non-web apps

Updated · 6 min read · By APM Tips editorial team

In the classic SDK, performance counters come from PerformanceCollectorModule, which reads Windows performance counters: it needs Windows, a module that is actually registered, and permission to read counters. On Linux and in Linux containers SDK 2.8.0 and later collects only CPU and memory, and EventCounters are the cross-platform route. SDK 3.x and the OpenTelemetry distros collect a fixed set of counters themselves, on by default, into the performanceCounters table, and .NET runtime metrics such as GC and thread pool come from the System.Runtime meter.

“No data for process CPU” is one of the most common questions about Application Insights outside web apps. The answer depends on three things: which SDK you use, which operating system the process runs on, and whether anything in your startup code registered the component that reads counters.

Where counters come from

PathMechanismLands in
Classic SDK 2.x on WindowsPerformanceCollectorModule reads Windows performance countersperformanceCounters
Classic SDK 2.x, cross-platformEventCounterCollectionModule reads .NET EventCounterscustomMetrics
SDK 3.xBuilt-in collection, switch EnablePerformanceCounterCollectionModuleperformanceCounters
Microsoft OpenTelemetry DistroBuilt-in collection, switch EnablePerfCountersperformanceCounters
Any OpenTelemetry setupMeters you register, such as System.RuntimecustomMetrics

In the workspace, performanceCounters is AppPerformanceCounters and customMetrics is AppMetrics.

Classic SDK 2.x

The .NET Classic API SDK 2.x is deprecated and retires on March 31, 2027.

Windows performance counters

PerformanceCollectorModule, in Microsoft.ApplicationInsights.PerfCounterCollector, is Windows-only. For ASP.NET Core apps its default set is process CPU (raw and normalised), available memory, private bytes, process I/O and total processor time; classic ASP.NET adds request rate, request execution time, requests in queue and the CLR exceptions-thrown rate.

Three environment details decide whether you see them:

  • IIS app pools need permission. Add the pool identity to the Performance Monitor Users group: net localgroup "Performance Monitor Users" /add "IIS APPPOOL\NameOfYourPool".
  • App Service runs a sandbox. Apps can’t read system counters directly there; the SDK collects a subset exposed by the platform. Windows containers on App Service get all standard counters.
  • ASP.NET Core support depends on version and target. SDK 2.7.1 and later collects counters on Windows for apps targeting netstandard2.0 or later; SDK 2.8.0 and later supports only the CPU and memory counters on Linux.

Console apps and Windows services

Here is the usual cause of empty counters: a console app that references only Microsoft.ApplicationInsights has no modules at all. The simplest fix is the Worker Service package, which registers the performance collector along with dependency tracking and Live Metrics:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddApplicationInsightsTelemetryWorkerService(o =>
    o.ConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"));
using var provider = services.BuildServiceProvider();

Hosted services built on the .NET generic host make the same call on builder.Services. If you build TelemetryConfiguration yourself instead, initialise the module with that configuration:

using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector;

var perfModule = new PerformanceCollectorModule();
perfModule.Counters.Add(new PerformanceCounterCollectionRequest(
    @"\Process(MyService)\Handle Count", "Handle count"));
perfModule.Initialize(configuration);

Counter paths use \Category(instance)\Counter, or \Category\Counter for categories without instances. A ReportAs name is required when the counter name contains characters outside [a-zA-Z()/-_ \.], and an instance becomes a CounterInstanceName dimension. Short-lived tools also need to flush before exit; see telemetry channels.

Linux and containers: EventCounters

EventCounters are .NET’s cross-platform counters, and EventCounterCollectionModule collects them on any OS without special permissions. It’s enabled automatically by the ASP.NET Core and Worker Service SDKs, but since version 2.15.0 it collects no counters by default, so add the ones you want:

using Microsoft.ApplicationInsights.Extensibility.EventCounterCollector;

builder.Services.ConfigureTelemetryModule<EventCounterCollectionModule>((module, o) =>
{
    module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "gen-0-size"));
    module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "threadpool-queue-length"));
});

They’re collected every 60 seconds (not configurable) and stored in customMetrics under the provider and the counter’s display name, such as System.Runtime|ThreadPool Completed Work Item Count. The .NET documentation lists every runtime counter and its short name.

Today: SDK 3.x and the Microsoft OpenTelemetry Distro

SDK 3.x has no 3.x version of Microsoft.ApplicationInsights.PerfCounterCollector or Microsoft.ApplicationInsights.EventCounterCollector; remove both when you upgrade. Performance counter collection is built into the 3.x packages and controlled by EnablePerformanceCounterCollectionModule, which is on by default. EventCounter collection is discontinued with no direct replacement: use OpenTelemetry meters instead.

The Microsoft OpenTelemetry Distro for .NET, Node.js and Python (and the Azure Monitor distro for Java) exports a fixed set of counters to performanceCounters. In .NET, options.AzureMonitor.EnablePerfCounters controls it and defaults to true.

Counter nameMeaning
% Processor TimeProcess CPU (category Process) or total machine CPU (category Processor)
% Processor Time NormalizedProcess CPU divided by logical processor count
Private BytesPrivate memory of the process
Available BytesPhysical memory available to processes
IO Data Bytes/secProcess I/O throughput
Requests/Sec, Request Execution Time, Requests In Application QueueRequest rate, average duration, queue length
# of Exceps Thrown / secException rate

The Performance and Failures views use available memory, process CPU and process I/O, and smart detection uses private bytes, which is one more reason not to switch them off.

Runtime metrics such as GC collections, heap size and thread pool queue length come from the .NET System.Runtime meter, available automatically in .NET 9 and later. Register it with the distro to export it:

builder.UseMicrosoftOpenTelemetry(o => o.Exporters = ExportTarget.AzureMonitor);
builder.Services.ConfigureOpenTelemetryMeterProvider((sp, metrics) =>
    metrics.AddMeter("System.Runtime"));

These land in customMetrics under their OpenTelemetry names, such as dotnet.gc.collections. For billing, Microsoft treats all OpenTelemetry metrics as Application Insights custom metrics. The same pattern registers your own meters, as in custom metrics; the pricing guide covers what custom metrics cost.

Query the counters

List what’s being collected, then chart one counter per instance:

performanceCounters
| where timestamp > ago(1d)
| summarize samples = count(), avg_value = avg(value) by category, counter, instance
| order by category asc, counter asc
performanceCounters
| where timestamp > ago(1d)
| where name == "% Processor Time Normalized"
| summarize avg(value) by cloud_RoleInstance, bin(timestamp, 5m)
| render timechart

instance is the counter instance (usually a process name), not the server; the server is cloud_RoleInstance. In the workspace the same columns are Category, Counter, Instance, Name, Value and AppRoleInstance. Counters are never sampled, so plain averages are correct here, unlike request counts; more queries are in the KQL library.

Watch them live, alert on them later

Two quick checks tell you whether counters are flowing before you write any query. Live Metrics shows process CPU and memory within a second of the app starting, for every connected instance, and doesn’t depend on ingestion at all; if the server shows there but performanceCounters stays empty, the problem is in collection settings rather than connectivity. The Live Metrics guide covers setup. The Metrics pane of the Application Insights resource charts the default set of counters without any query.

For alerting, Microsoft points to the same mechanism as for any other metric: an alert rule that fires when a counter crosses a limit. Because counters are never sampled, a threshold on process CPU or private bytes is reliable even on a heavily sampled app. Alert on the normalised CPU counter when instances have different core counts.

Troubleshooting

SymptomLikely causeFix
No counters at all from a console app (2.x)Only the core package; no module registeredUse the Worker Service package or initialise the module
Counters on Windows dev box, none in Linux container (2.x)Windows counters don’t exist on LinuxExpect CPU and memory only; add EventCounters
Counters missing under IISApp pool can’t read countersAdd it to Performance Monitor Users
A custom counter never appearsWrong path or instance name, or ReportAs missingCheck with Get-Counter -ListSet * on the server
EventCounters configured but nothing in customMetricsNo counters added (none by default since 2.15.0)Add EventCounterCollectionRequest entries
Counters stopped after upgrading to 3.xEnablePerformanceCounterCollectionModule set to false, or old packages left inRemove 2.x collector packages; check the switch

Queries for this topic

From the KQL library: explained queries for the Application Insights tables, ready to copy.

  • Returns: Two lines per instance over the last 24 hours in 5-minute steps, the process CPU divided by the processor count and the machine's available memory in gigabytes.

  • Returns: One row per service and application version with its sampling-adjusted request count, the number and names of instances that served it, and when it was first and last seen in the window.

Frequently asked questions

Why is process CPU empty for my app on Linux?

With the classic SDK, Windows performance counters don't exist on Linux. SDK 2.8.0 and later collects only the CPU and memory counters there; anything else needs EventCounters. On SDK 3.x or the OpenTelemetry distros, check that performance counter collection hasn't been switched off.

Are performance counters sampled?

No. Microsoft states that performance counters, like metrics and sessions, are never sampled by any of the sampling methods, so their values are complete even when requests are sampled heavily.

Where do EventCounters show up?

In the classic SDK, EventCounters collected by EventCounterCollectionModule land in the customMetrics table, named with the provider and counter, for example System.Runtime|ThreadPool Completed Work Item Count. They're collected every 60 seconds, and that interval can't be changed.

Sources, checked

  1. Monitor .NET and Node.js applications with Application Insights (Classic API 2.x, counters)learn.microsoft.com · checked
  2. Metrics in Application Insights (performance counters)learn.microsoft.com · checked
  3. Migrate Application Insights Classic API SDKs to OpenTelemetrylearn.microsoft.com · checked
  4. Breaking changes: Application Insights 2.x to 3.xgithub.com · checked
  5. Microsoft.OpenTelemetry (Microsoft OpenTelemetry Distro for .NET), READMEgithub.com · checked
  6. .NET runtime metrics (System.Runtime meter)learn.microsoft.com · checked
  7. Well-known EventCounters in .NETlearn.microsoft.com · checked
  8. AppPerformanceCounters table referencelearn.microsoft.com · checked