A Prometheus exporter for Windows hardware metrics using LibreHardwareMonitor.
A ready-to-use Grafana dashboard is included at docs/grafana-dashboard.json. Import it via Grafana UI (Dashboards > Import > Upload JSON file).
Dashboard sections: CPU Overview, CPU Temperature, CPU Power & Voltage, Fans, Memory, GPU (Temperature / Load / VRAM / Power / Clock / PCIe), Disk (Temperature / Space / Health / IO / Throughput), Network (Utilization / Throughput / Data Transferred).
Until ee6b1c2 (2026-05-08), LHM-derived metrics were emitted as
hardware_<type>_<sensor_type>_<name> carrying LHM's native units (GB for
Data, MB for SmallData, B/s for Throughput). That naming was
unit-implicit and the Data variant being GB-quantized integer made
rate(hardware_storage_data_*[5m]) return 0 over short windows.
The exporter now emits Prometheus-conventional aliases in parallel with
the legacy form: *_bytes_per_second for Throughput sensors and *_bytes
for Data / SmallData sensors, with values normalized to base SI units.
The legacy form will be removed in a release tagged on or after
2026-08-15. See CHANGELOG.md for the full mapping table.
To prepare:
- Re-import
docs/grafana-dashboard.json— it was migrated to aliases on 2026-05-08. - Audit custom dashboards / alert rules:
Anything returned is on a name that will go away.
{__name__=~"hardware_(storage|memory|network|gpu)_(throughput|data)_.*"} - Set
HardwareMonitor:EmitLegacyMetricNamestofalseinappsettings.jsonto preview the post-removal/metricsoutput without actually removing anything yet.
Sensors without a unit (temperature, fan, voltage, power, clock, load,
level, factor) are unaffected — their legacy form is the only form, and
the EmitLegacyMetricNames flag does not silence them.
LibreHardwareMonitor cannot read SMART data from disks attached behind SAS HBAs (LSI/Avago, Adaptec, etc.) because those disks appear as SCSI devices and require SAT (SCSI/ATA Translation) pass-through that the library does not implement. To cover that gap, this exporter bundles smartctl and emits a parallel set of metrics under the hardware_storage_smart_* prefix.
Bundled binaries live in <install-dir>\smartctl\ (≈1.4 MB total). They are invoked as a separate process; smartmontools is GPLv2+ and is included as a separate, unmodified executable. See smartctl\COPYING.txt and smartctl\README.txt for full attribution.
To use a different smartctl.exe (e.g. a newer version), set SmartMonitor:SmartctlPath in appsettings.json. To disable the SMART collector entirely, set SmartMonitor:Enable to false.
# Drive temperature, including SAS HBA-attached SATA disks
hardware_storage_smart_temperature_celsius{device="/dev/sda", model="WDC WUH721816ALE6L4", serial="3HGHU16P", protocol="ata", firmware="PCGNW232"} 34
# Overall self-assessment (1 = passed, 0 = failed)
hardware_storage_smart_health_passed{device="/dev/sda", ...} 1
# ATA SMART attribute table (per-id)
hardware_storage_smart_ata_attribute_raw{device="/dev/sda", ..., id="5", name="Reallocated_Sector_Ct"} 0
hardware_storage_smart_ata_attribute_raw{device="/dev/sda", ..., id="9", name="Power_On_Hours"} 31120
# NVMe-specific
hardware_storage_smart_nvme_percentage_used{device="/dev/sdi", model="SAMSUNG MZWLL3T2HAJQ-00005", ...} 6
hardware_storage_smart_nvme_media_errors{device="/dev/sdi", ...} 0
| Option | Type | Default | Description |
|---|---|---|---|
SmartMonitor:Enable |
bool | true | Enable the SMART collector |
SmartMonitor:SmartctlPath |
string | "" |
Override smartctl.exe path (empty = bundled) |
SmartMonitor:RefreshIntervalSeconds |
int | 60 | How often to re-poll smartctl. Refresh is asynchronous; /metrics always serves the most recent cached snapshot, so Prometheus scrape latency is unaffected. Note: reading SMART can wake idle HDDs, so don't set this too aggressively. |
SmartMonitor:InvocationTimeoutSeconds |
int | 15 | Per-disk timeout for a single smartctl call |
SmartMonitor:DeviceExcludePatterns |
string[] | [] |
Glob patterns matched against /dev/sdN names; matching devices are skipped |
Both collectors run their own background refresh loop and serve /metrics from an in-memory cache. The Prometheus scrape never triggers a hardware read directly. There are two independent caches:
| Cache | Config option | Default | Lower bound |
|---|---|---|---|
| LibreHardwareMonitor (CPU / GPU / memory / motherboard / network / native SATA SMART / NVMe) | HardwareMonitor:ScrapeIntervalSeconds |
15s | 1s |
| smartctl (full SMART table for every disk, incl. those behind SAS HBA) | SmartMonitor:RefreshIntervalSeconds |
60s | 15s |
Both fire one eager refresh at service start, so the first scrape after launch already has data — no warmup gap.
Why two different defaults
HardwareMonitorcovers fast-changing telemetry (CPU temperature, GPU load, fan RPM). 15s matches the default Prometheus scrape interval, so client-visible staleness stays imperceptible.SmartMonitorreads slow-changing health attributes (temperature drifts in minutes, reallocated-sectors in days). More importantly, every smartctl invocation against an idle HDD spins it back up — running it on the scrape path would prevent disks from ever sleeping. 60s is the floor where that's actually tolerable.
Effective staleness
What a Prometheus client sees is bounded by cache_interval + scrape_interval. With defaults:
- LibreHardwareMonitor metrics: ≤ 30 s old
- SMART metrics: ≤ 75 s old
When to retune
- Want a 1Hz dashboard: set
HardwareMonitor:ScrapeIntervalSeconds=1. Don't touchSmartMonitor— 15s is already the lower bound and it'd thrash idle drives. - Have AHCI-attached SATA HDDs you want to actually spin down: bump
HardwareMonitor:ScrapeIntervalSecondsto ≥ 60s. LHM'sUpdate()issuesIOCTL_ATA_PASS_THROUGH SMART READ DATAto those disks, which counts as activity for ATAstandby_timer. (SAS-HBA-attached disks are unaffected — LHM can't read SMART through SAT, only perfmon, which doesn't wake the disk.) - NVMe-only host with no power concerns: defaults are fine.
windows_exporter's thermal zone data is not accurate
This exporter provides accurate hardware monitoring data by using LibreHardwareMonitor library directly.
- ✅ Accurate CPU, GPU, Memory, Motherboard, Network, and Storage metrics
- ✅ GPU support for NVIDIA, AMD, and Intel (including Arc discrete GPUs)
- ✅ Runs as Windows Service
- ✅ Configurable hardware monitoring (enable/disable specific components)
- ✅ Prometheus-compatible metrics format
- ✅ Low resource usage with singleton pattern
- ✅ Structured logging
- Windows 10/11 or Windows Server 2016+
- .NET 10.0 Runtime (or SDK for development)
- Administrator privileges (required for hardware access)
LibreHardwareMonitor reads CPU MSRs (temperature, voltage) and the motherboard Super-I/O chip (fan speeds, board voltages) through a ring-0 kernel driver. Since LibreHardwareMonitor 0.9.5 that driver is PawnIO — a signed, HVCI/Memory-Integrity-compatible replacement for the old WinRing0 driver.
PawnIO is bundled with this exporter and installed automatically. The MSI
installer (and install.ps1) runs HardwareExporterWindows.exe --install-pawnio,
which creates the Root\PawnIO device and installs the driver package in
<install-dir>\pawnio\ — headless, no extra tooling. The step is idempotent
(skipped if PawnIO is already present) and non-fatal (a failure does not abort
the install).
If PawnIO is not installed, the exporter still runs and still reports GPU,
memory, network, storage and SMART metrics — but hardware_cpu_temperature_*,
hardware_cpu_voltage_* and all hardware_motherboard_* sensors are absent.
It degrades; it does not fail.
Running from source (
dotnet run) does not install PawnIO. For development, install it once from https://pawnio.eu/, or runHardwareExporterWindows.exe --install-pawniofrom an elevated prompt.
PawnIO is GPLv2 — bundled as a separate, unmodified, signed driver; see
<install-dir>\pawnio\COPYING and pawnio\README.txt.
Option 1: MSI Installer (Recommended)
- Download
HardwareExporterWindows-win-x64.msifrom the latest release - Double-click the MSI file to install
- The installer will automatically:
- Copy files to
C:\Program Files\HardwareExporter - Install and start the Windows Service
- Configure firewall rules
- Copy files to
Option 2: Manual Installation with PowerShell Script
- Download
HardwareExporterWindows-win-x64.zipfrom the latest release - Extract to
C:\Program Files\HardwareExporter - Run PowerShell as Administrator
- Execute the installation script:
cd "C:\Program Files\HardwareExporter"
.\install.ps1The script will:
- Copy files to the installation directory
- Create a Windows Firewall rule
- Register and start the Windows Service
# Register as Windows Service
New-Service -Name "HardwareExporter" `
-BinaryPathName "C:\Program Files\HardwareExporter\HardwareExporterWindows.exe" `
-StartupType "Automatic" `
-Description "Hardware Exporter Service"
# Start the service
Start-Service -Name "HardwareExporter"Edit appsettings.json to customize the exporter:
{
"HardwareMonitor": {
"EnableCpu": true,
"EnableGpu": true,
"EnableMemory": true,
"EnableMotherboard": true,
"EnableController": true,
"EnableNetwork": true,
"EnableStorage": true,
"ScrapeIntervalSeconds": 15
},
"Urls": "http://0.0.0.0:9888"
}| Option | Type | Default | Description |
|---|---|---|---|
EnableCpu |
bool | true | Monitor CPU metrics |
EnableGpu |
bool | true | Monitor GPU metrics |
EnableMemory |
bool | true | Monitor Memory metrics |
EnableMotherboard |
bool | true | Monitor Motherboard metrics |
EnableController |
bool | true | Monitor Controller metrics |
EnableNetwork |
bool | true | Monitor Network metrics |
EnableStorage |
bool | true | Monitor Storage metrics |
ScrapeIntervalSeconds |
int | 15 | How often the background loop calls Update() on the hardware monitor. /metrics always serves the most recent cached snapshot, so this controls hardware-poll frequency, not Prometheus scrape latency. Lower = fresher data; higher = idle AHCI HDDs can actually spin down (each Update() issues ATA SMART pass-through to those disks). |
Add this to your prometheus.yml:
scrape_configs:
- job_name: 'windows-hardware'
static_configs:
- targets:
- '192.168.1.100:9888' # Your Windows machine IP
- '192.168.1.101:9888'HardwareExporterWeb is an optional companion service that provides automatic service discovery for Prometheus. It is not related to metrics collection - it only helps Prometheus automatically discover monitoring targets on your network.
- Automatically scans your local network to discover Windows machines running HardwareExporter
- Provides Prometheus HTTP Service Discovery endpoints
- Eliminates the need to manually configure each target in
prometheus.yml
Download HardwareExporterWeb-win-x64.zip from the latest release and run it on any machine in your network (doesn't need to be on every monitored machine).
Edit appsettings.json:
{
"NetworkScan": {
"SubnetFilter": "", // Empty = scan all local subnets
"SubnetMask": "255.255.255.0" // Subnet mask
}
}Instead of static targets, use HTTP service discovery:
scrape_configs:
- job_name: 'windows-hardware-auto'
http_sd_configs:
- url: 'http://your-web-server/api/ServiceDiscovery/HardwareExporter'
refresh_interval: 60s/api/ServiceDiscovery/HardwareExporter- Discover HardwareExporter instances/api/ServiceDiscovery/WindowsExporter- Discover windows_exporter instances
Note: This is completely optional. You can use static configuration in prometheus.yml if you prefer.
The exporter provides two types of metrics:
- Hardware Metrics - From LibreHardwareMonitor
- .NET Runtime Metrics - From prometheus-net (GC, threads, memory, etc.)
All metrics are available at the /metrics endpoint.
LHM-derived metrics carry the LHM SensorType verbatim and use LHM's native units (see the "Metric Types" table below):
hardware_{type}_{sensor_type}_{sensor_name}{labels} value
In addition, for sensor types that carry a unit (Throughput, Data,
SmallData), the exporter also emits a Prometheus-conventional alias with
values normalized to base SI units. Prefer these for new dashboards — they
need no per-panel unit fiddling and sidestep the GB-quantization that makes
rate(hardware_storage_data_*[5m]) unreliable.
hardware_{type}_{sensor_name}_bytes_per_second{labels} value # Throughput
hardware_{type}_{sensor_name}_bytes{labels} value # Data, SmallData
# CPU Temperature
hardware_cpu_temperature_core{name="AMD Ryzen 9 5900X", core="0"} 45.0
# GPU Usage
hardware_gpu_load_core{name="NVIDIA GeForce RTX 3080", vendor="nvidia"} 75.5
# Memory Usage (legacy, GB)
hardware_memory_data_memory_used{name="Generic Memory"} 18.655
# Same value, conventional alias (bytes)
hardware_memory_used_bytes{name="Generic Memory"} 18655895233
# Fan Speed
hardware_motherboard_fan_fan{name="ASUS ROG STRIX B550-F", fan="1"} 1200
# Disk throughput (legacy, B/s — note: NOT MB/s, was a doc bug)
hardware_storage_throughput_write_rate{name="KIOXIA-EXCERIA SSD"} 153991
# Same value, conventional alias
hardware_storage_write_rate_bytes_per_second{name="KIOXIA-EXCERIA SSD"} 153991
# Lifetime data written (legacy, GB cumulative — NOT a rate)
hardware_storage_data_written{name="SAMSUNG MZWLL3T2HAJQ"} 4608505
# Same value, conventional alias (bytes cumulative; safe to use rate())
hardware_storage_data_written_bytes{name="SAMSUNG MZWLL3T2HAJQ"} 4608505000000000
| LHM SensorType | Legacy unit (in hardware_*_<sensor_type>_*) |
Conventional alias suffix | Alias value |
|---|---|---|---|
Temperature |
°C | _ | _ |
Load |
percent (0-100) | _ | _ |
Clock |
MHz | _ | _ |
Power |
W | _ | _ |
Fan |
RPM | _ | _ |
Voltage |
V | _ | _ |
Throughput |
B/s (not MB/s — earlier docs were wrong) | _bytes_per_second |
same value (already B/s) |
Data |
GB cumulative (not GB/s) | _bytes |
legacy × 1e9 |
SmallData |
MB cumulative | _bytes |
legacy × 1e6 |
Energy |
mWh | _ | _ |
- Check Windows Event Viewer for errors
- Ensure you have Administrator privileges
- Verify .NET 10.0 Runtime is installed
- Check if port 9888 is already in use
- Check if the service is running:
Get-Service HardwareExporter - Test the endpoint:
curl http://localhost:9888/metrics - Check logs in Event Viewer under "Application"
If /metrics has GPU, memory, storage and network data but no
hardware_cpu_temperature_*, hardware_cpu_voltage_* or
hardware_motherboard_* series, the PawnIO kernel driver did not install.
Re-run the install step from an elevated prompt and restart the service:
& "C:\Program Files\HardwareExporter\HardwareExporterWindows.exe" --install-pawnio
Restart-Service HardwareExporter
Get-Service PawnIO # should be RunningThe installation script creates a firewall rule automatically. If needed, create it manually:
New-NetFirewallRule -DisplayName "HardwareExporter" `
-Direction Inbound `
-Program "C:\Program Files\HardwareExporter\HardwareExporterWindows.exe" `
-Action Allowgit clone https://github.com/naughtyGitCat/HardwareExporterWindows.git
cd HardwareExporterWindows
dotnet buildcd src/HardwareExporterWindows
dotnet runAccess metrics at: http://localhost:9888/metrics
dotnet test- LibreHardwareMonitor - Hardware monitoring library
- prometheus-net - Prometheus client library
MIT License - see LICENSE file for details.
This project uses the following open source libraries:
- LibreHardwareMonitor (MPL 2.0)
- prometheus-net (MIT)
Contributions are welcome! Please feel free to submit a Pull Request.
