My laptop had become painful to use. Chrome was sluggish, switching tabs stuttered, and even opening a terminal felt heavy. So I did what I should have done on day one: I stopped guessing and measured. The answer had nothing to do with RAM, disk or Chrome.
The symptom
The usual suspects were easy to check, and all of them were innocent:
- Memory: 30 GB installed, 26 GB free, zero bytes in swap.
- Disk: an SSD with ~0% I/O wait.
- Load: a few dev tools running, but the machine was over 80% idle.
Nothing was obviously broken. The one number I had not looked at was the one that explains everything: how fast the CPU is actually running.
Step 1: read the current clock
Two commands are enough on any Linux machine:
grep MHz /proc/cpuinfo
lscpu | grep -E 'Model name|MHz'Mine answered 800 MHz, on all twelve threads. The processor is an Intel Core i7-9750H: a 2.6 GHz part that boosts to 4.5 GHz. It was running at about one third of its base clock.
An idle CPU sitting at 800 MHz is completely normal, that is power saving. The fault only becomes visible once you put it under load.
Step 2: find the ceiling the CPU is allowed to reach
Before blaming the hardware, rule out the operating system. Linux exposes both the hardware limits and the limits it is currently enforcing:
# what the CPU says it can do (kHz)
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
# what the OS currently allows (kHz)
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
# Intel P-state limits, as a percentage of the maximum
cat /sys/devices/system/cpu/intel_pstate/{min_perf_pct,max_perf_pct,no_turbo}Every one of those answered go ahead: a 4,500,000 kHz ceiling, max_perf_pct=100, turbo not disabled. On paper the CPU was free to run at full speed.
| Layer | Reading | Verdict |
|---|---|---|
| CPU rated maximum | 4,500 MHz | hardware is capable |
| OS frequency ceiling | 4,500 MHz | not restricted |
| Actual clock, full load | 800 MHz | 🔴 the anomaly |
Step 3: prove it under load
Put every core to work for a few seconds and read the clock again. This is the test that separates a power-saving illusion from a real fault:
for i in $(seq 1 $(nproc)); do yes > /dev/null & done
sleep 5
grep MHz /proc/cpuinfo
pkill yesMine stayed at 800 MHz with 100% CPU utilisation and a load average of 11, while the package temperature was only 52 °C.
That combination is the whole diagnosis: maximum load, minimum clock, low temperature. A genuinely hot CPU throttles because it is approaching 100 °C. This one was cool and still refused to run. Something outside the scheduler was holding it down.
Same machine, same stress test. Before, the clock never moved. After, it tracks the load and pulls back only when it gets hot.
Step 4: ask the CPU why
Intel processors publish the reason they are being limited in a model-specific register (MSR). Install msr-tools and read them:
sudo apt install msr-tools # rdmsr / wrmsr
sudo modprobe msr
sudo rdmsr -p 0 0x64F # PERF_LIMIT_REASONS: why am I being limited?
sudo rdmsr -p 0 0x1FC # POWER_CTL: do I listen to PROCHOT?
sudo rdmsr -p 0 0x198 # PERF_STATUS: what ratio am I running at?
sudo rdmsr -p 0 0x199 # PERF_CTL: what ratio was requested?Mine answered:
| Register | Value | Meaning |
|---|---|---|
| 0x64F, PERF_LIMIT_REASONS | 0x00010001 | bit 0 set, PROCHOT is the active limiter |
| 0x1FC, POWER_CTL | 0x002c005d | bit 0 = 1, listening to the external PROCHOT pin |
| 0x198, PERF_STATUS | 0x…0800 | current ratio 8, so 800 MHz |
| 0x199, PERF_CTL | 0x…2d00 | requested ratio 45, so 4.5 GHz |
Requested 4.5 GHz, delivered 0.8 GHz, and the CPU named the reason itself: PROCHOT.
What PROCHOT actually is
PROCHOT# (Processor Hot) is a physical pin on the CPU package. When it is pulled low, the processor drops to its minimum P-state immediately, with no negotiation and no involvement from the operating system. Intel also implements Bi-Directional PROCHOT (BD PROCHOT): the CPU listens to that pin not only for its own temperature, but for signals from other components, such as voltage regulators, the embedded controller and the battery.
In principle that is a good safety feature: if power delivery cannot sustain the CPU, something must slow it down. It becomes a bug when the signal is held down while nothing is hot.
The OS asks for 4.5 GHz. The pin wins.
Step 5: rule out the cheap causes
Before reaching for a workaround, work out why the signal is being pulled. In rough order of likelihood:
- The power adapter or the batteryOn many laptops a failing battery or an underpowered adapter makes the embedded controller assert PROCHOT to protect the voltage regulators. Test it: repeat the load test with the charger unplugged.
- Real thermalsRun
sensors. The package should sit well under its critical temperature. If it reads 95 to 100 °C, the CPU is genuinely hot, so clean the fans and repaste instead. - Power profile or governorCheck
powerprofilesctlorcpupower frequency-info. Apowersavegovernor with a lowmax_perf_pctcan look similar, but it will respond to load. - A latched embedded controllerA full power drain resets it: shut down, unplug the charger, hold the power button for 30 to 60 seconds, plug back in and boot.
- FirmwareAn outdated BIOS/EC is a very common source of spurious PROCHOT on gaming laptops. Check your vendor support page for a BIOS newer than the one you run.
In my case the pin stayed asserted on both AC and battery, the battery reported 97% health with 83 charge cycles, and the BIOS was seven years old. That pointed at the embedded controller and firmware rather than a dying cell.
Step 6: the workaround that gave me the speed back
Once the real faults are ruled out and the signal is still firing, you can tell the CPU to stop listening to BD PROCHOT by clearing bit 0 of MSR 0x1FC. Every core keeps its own copy, so write all of them:
sudo rdmsr -a 0x1FC # read, usually ends in 5d or 5b
sudo wrmsr -a 0x1FC 0x2c005c # clear bit 0, ...5d becomes ...5cThe change takes effect immediately but does not survive a reboot, so make it persistent with a small systemd unit:
sudo tee /usr/local/bin/fix-bd-prochot.sh >/dev/null <<'EOF'
#!/bin/bash
modprobe msr 2>/dev/null || true
for i in $(seq 0 $(( $(nproc) - 1 ))); do
cur=$(rdmsr -p "$i" 0x1FC 2>/dev/null) || continue
if (( 0x$cur & 1 )); then
wrmsr -p "$i" 0x1FC "$(printf '0x%x' $(( 0x$cur & ~1 )))"
fi
done
EOF
sudo chmod 755 /usr/local/bin/fix-bd-prochot.sh[Unit]
Description=Disable BD PROCHOT (workaround: CPU pinned at 800MHz)
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/fix-bd-prochot.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now bd-prochot-fix.serviceThe result
Immediately after clearing the bit, the same full-load test reported 3.1 GHz across all twelve threads at 94 °C, and normal desktop work now boosts to around 4 GHz. Chrome stopped stuttering. Idle still drops back to 800 MHz, so power saving is intact; only the artificial ceiling is gone.
If you are on Windows
- ThrottleStop: untick BD PROCHOT in the main window. That is the same bit, wrapped in a GUI.
- HWiNFO64: watch Core Clocks, CPU Package Power and the PROCHOT flag.
powercfg /qhlists the processor performance limits Windows is applying.- AMD systems use different registers, so look at Ryzen Master or
ryzen_monitorfor PROCHOT/HTC flags.
TL;DR checklist
- Read the current clock:
grep MHz /proc/cpuinfo. - Stress every core for five seconds and read it again. Low at idle is normal; low under load is not.
- Check the ceiling the OS allows:
cpuinfo_max_freq,scaling_max_freq,max_perf_pct. - If the OS allows full speed but the clock will not move, read
rdmsr 0x64F. The CPU will name the reason. - If the reason is PROCHOT while temperatures are low: test the adapter and battery, reset the embedded controller, then update the BIOS.
- Only then consider clearing BD PROCHOT (bit 0 of
0x1FC) as a workaround.