![My Laptop's CPU stuck at Max Turbo 24/7 [ Here's Why ]](https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmvfe0nsrjkgxh0kjvu7n.png)
Muhammad BilalTL;DR: My HP EliteBook 840 G2 was running its CPU at max turbo (2900 MHz) around the clock, even at...
TL;DR: My HP EliteBook 840 G2 was running its CPU at max turbo (2900 MHz) around the clock, even at idle, cooking itself at 83–88°C and throttling on the RAPL power limit over 115,000 times in two hours. I traced it into intel_pstate.c, was fairly convinced I'd found a kernel bug, wrote up a whole Bugzilla report... and then the kernel documentation politely told me I hadn't. Here's the full investigation, the logs, the wrong turn, and the actual fix.
I'd had this laptop running tuned's throughput-performance profile for a while, and at some point I noticed something that should have bothered me a lot sooner: the fans never really spun down. Not under load --- that's expected --- but at idle. I was running a background session with maybe 20–25% CPU busy, and the machine was behaving like it was under sustained full load.
First stop, obviously: cpupower / raw sysfs.
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
2900000
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
2900000
scaling_min_freq equal to scaling_max_freq equal to cpuinfo_max_freq. The governor had exactly one option: max turbo, always. On an i5-5300U (Broadwell-U, no HWP, 15 W RAPL PL1), that's not a "fast" configuration, it's a "the CPU is going to spend its whole life getting power-limited and thermal-throttled" configuration.
Here's the full snapshot from that first pass:
Full BEFORE snapshot (cpufreq + intel_pstate sysfs, RAPL, cpuidle, throttle counters)
Machine: HP EliteBook 840 G2, Intel Core i5-5300U (Broadwell-U, 4 threads, no HWP exposed)
Kernel: 7.2.0-rc5+ (vanilla drivers/cpufreq, verified against git only local commit was
an unrelated hp-bioscfg fix, 105d2f15be0d)
Config: CONFIG_X86_INTEL_PSTATE=y, CONFIG_CPU_FREQ_DEFAULT_GOV_SCHEDUTIL=y
Tuned: profile "throughput-performance" applied at boot (tuned.log 2026-08-02 21:41:17)
--- cpufreq sysfs (cpu0) ---
scaling_driver = intel_cpufreq (intel_pstate PASSIVE mode)
scaling_governor = performance
cpuinfo_min_freq = 500000
cpuinfo_max_freq = 2900000
scaling_min_freq = 2900000 <- floor pinned at MAX TURBO
scaling_max_freq = 2900000
--- intel_pstate sysfs ---
max_perf_pct = 100
min_perf_pct = 100
no_turbo = 0
num_pstates = 25
status = passive
turbo_pct = 25
--- thermal_throttle counters (accumulated in ~2h uptime) ---
cpu0: core_throttle_count=115754 package_throttle_count=115803
cpu1: core_throttle_count=115754 package_throttle_count=115803
cpu2: core_throttle_count=45 package_throttle_count=115807
cpu3: core_throttle_count=45 package_throttle_count=115804
=> ~115,800 package power-limit throttles in 2h. The package hits the RAPL
PL1 limit constantly because it's never allowed to drop below max turbo.
--- RAPL / powercap ---
intel-rapl:0 (package-0): constraint_0 (long_term) power_limit_uw = 15000000 (15 W)
constraint_1 (short_term) power_limit_uw = 25000000 (25 W)
enabled = 1
--- cpuidle states (cpu0) ---
POLL lat=0 use=8915 | C1 lat=2 use=64327 | C1E lat=10 use=284758
C3 lat=40 use=776488 | C6 lat=133 use=283868 | C7s lat=166 use=1028136
C8 lat=300 use=95112 | C9 lat=600 use=380983 | C10 lat=2600 use=52770
--- cmdline ---
BOOT_IMAGE=(hd0,gpt2)/vmlinuz-7.2.0-rc5+ root=UUID=... ro rootflags=subvol=root rhgb quiet kasan_multi_shot
The min_perf_pct = 100 line is the one that matters, and I'll come back to it.
Sysfs tells you what the driver thinks it's doing; turbostat tells you what the silicon is actually doing. Ran it during a light-load phase (~22–25% busy):
turbostat, "broken" phase (~22–25% busy)
Core CPU Avg_MHz Busy% Bzy_MHz TSC_MHz IPC IRQ NMI SMI LLCkRPS LLC%hit POLL% C1% C1E% C3% C6%
C7s% C8% C9% C10% CPU%c1 CPU%c3 CPU%c6 CPU%c7 CoreTmp CoreThr PkgTmp GFX%rc6 GFXMHz GFXAMHz
Pkg%pc2 Pkg%pc3 Pkg%pc6 Pkg%pc7 Pkg%pc8 Pkg%pc9 Pk%pc10 PkgWatt CorWatt GFXWatt RAMWatt
691 24.80 2795 2294 1.07 8739 0 0 105515 59.44 0.00 0.27 0.54 6.78 4.86 26.51
2.28 31.04 2.37 18.93 7.14 5.26 43.87 83 0 84 85.64 300 300 9.95 2.92 3.44
12.73 0.00 0.00 0.00 6.11 4.69 0.04 1.34
631 22.52 2807 2296 1.06 8041 1 0 109715 60.89 0.01 0.21 0.49 5.91 4.55 28.69
2.00 32.89 2.24 18.43 6.34 4.98 47.73 88 0 88 86.01 300 300 8.25 2.83 2.00
17.67 0.00 0.00 0.00 5.80 4.44 0.03 1.34
640 22.86 2806 2294 1.11 7122 1 0 115673 63.19 0.00 0.08 0.35 4.85 4.01 28.57
2.20 32.86 3.70 17.98 5.16 4.58 49.41 87 0 87 85.43 300 300 7.93 2.12 1.90
18.52 0.00 0.00 0.00 5.83 4.48 0.04 1.33
CPU capabilities line, for reference:
CPUID(1): family:model:stepping 0x6:3d:4 (6:61:4) microcode 0x2f
CPUID(6): APERF, TURBO, DTS, PTM, No-HWP, No-HWPnotify, No-HWPwindow, No-HWPepp, No-HWPpkg, EPB
cpu0: MSR_PLATFORM_INFO: 5100=500 MHz max efficiency, 23100=2300 MHz base
cpu0: MSR_TURBO_RATIO_LIMIT: 27100=2700 (2-6 cores), 29100=2900 (1 core)
cpu0: cpufreq driver: intel_cpufreq
cpu0: cpufreq governor: performance
cpufreq intel_pstate no_turbo: 0
cpu0: EPB: 0 (performance)
cpu0: MSR_IA32_POWER_CTL: 0x0004005f (C1E auto-promotion: ENabled)
Bzy_MHz sitting at 2795–2807 while the machine is 22–25% busy. Core temps 83–88°C. That's not "using turbo when it helps," that's the driver never letting the CPU go below turbo at all. Under an actual sustained load test the frequency was capped to about 2.2–2.5 GHz by RAPL anyway, so there was zero throughput upside, the floor was pure heat and battery cost, no benefit.
The obvious next move: try to fix it myself.
$ echo 500000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
$ echo $?
0
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
2900000
The write succeeds return code 0, no error, nothing in dmesg, and the value doesn't change. Checked it again at 50ms, 200ms, 500ms, 1s, 3s after the write. Still 2900000. The documented, user-facing knob for controlling minimum CPU frequency was accepting input and just... doing nothing with it.
That's the kind of thing that gets a security/bug researcher's attention. A sysfs write that reports success but has no effect, with no diagnostic trail, is exactly the shape of bug I usually go looking for (just usually the outcome is "data corruption" instead of "annoyingly warm laptop").
So I went into drivers/cpufreq/intel_pstate.c to find out why.
In passive mode, intel_pstate calls itself intel_cpufreq and hands frequency selection off to the generic cpufreq governors, but it doesn't fully let go. During per-policy init, it registers its own min/max frequency constraints derived from the two global knobs (min_perf_pct / max_perf_pct), using the kernel's FREQ_QOS request mechanism:
freq = DIV_ROUND_UP(cpu->pstate.turbo_freq * global.min_perf_pct, 100);
ret = freq_qos_add_request(&policy->constraints, req, FREQ_QOS_MIN, freq);
...
freq = DIV_ROUND_UP(cpu->pstate.turbo_freq * global.max_perf_pct, 100);
ret = freq_qos_add_request(&policy->constraints, req + 1, FREQ_QOS_MAX, freq);
policy->driver_data = req;
With min_perf_pct = 100, that first line computes a minimum frequency request of turbo_freq * 100 / 100 i.e., max turbo. The driver registers that as a FREQ_QOS_MIN request against the policy. Every time you write to the global min_perf_pct sysfs file, store_min_perf_pct() refreshes this same request through update_cpu_qos_request().
Meanwhile, your scaling_min_freq write goes through generic cpufreq core (store_one() in drivers/cpufreq/cpufreq.c), which updates a separate FREQ_QOS_MIN request policy->min_freq_req representing the per-policy limit.
So now you have two independent FREQ_QOS_MIN requests sitting on the same policy: the driver's (from min_perf_pct) and yours (from scaling_min_freq). PM QoS has to resolve that down to one effective value, and for a minimum constraint, it resolves by taking the maximum of all registered minimums, which makes sense in isolation (if two parties both say "don't go below my number," the effective floor has to satisfy both, so you take the higher one). With min_perf_pct=100, the driver's request is already at the ceiling, so it wins against literally anything you write to scaling_min_freq. Your 500 MHz request never had a chance.
At this point I was fairly confident I'd found a real bug: a driver silently overriding a documented user control, no error surfaced, no way to discover it without already reading the driver source. I started writing it up.
I put together what I thought was a clean, evidence-backed Bugzilla submission, reproduction steps, the QoS mechanism, the turbostat data, suggested fix directions. Short version of what I had drafted for bugzilla.kernel.org, component Power Management:
intel_pstate(passive mode): the driver'sFREQ_QOS_MINrequest, derived from the globalmin_perf_pctknob, silently overrides the user'sscaling_min_freqwrite and pins the CPU at max turbo frequency untilmin_perf_pctis lowered.
Reproduction was clean and repeatable. Impact was measurable and real (the throttle counters and temps above). Suggested fix directions ranged from "make the driver's request track policy->min" to "at minimum, return an error from store_scaling_min_freq when the request can't be honored." I was about to go file it.
Then, out of the same habit that makes me grep the actual source before writing up any finding, I went and read the driver documentation properly instead of just the code. specifically Documentation/admin-guide/pm/intel_pstate.rst, section "Coordination of P-State Limits." And there it was, plainly stated:
none of them can be requested to run slower than the global minimum
That's rule #1 of the documented coordination model, and it applies "regardless of the current operation mode of the driver." Rule #3 explicitly adds that the global and per-policy limits are meant to be settable independently. which is exactly why my write returns rc=0 even though it gets overridden: it's not being rejected, it's succeeding as a request that then loses to a stricter, higher-priority global constraint. That's not an accident of implementation. That's the API working exactly as specified.
So the thing I'd found wasn't a bug. It was a correct, by-the-book implementation of a coordination rule I hadn't read yet. My QoS-request analysis was accurate I'd traced the actual mechanism correctly, I'd just drawn the wrong conclusion from it. I deleted the Bugzilla draft.
I think this is worth admitting outright rather than quietly editing out of the story: I nearly filed a "kernel bug" that a maintainer would have closed in about thirty seconds with a link to documentation I should have read first. It happens. The fix is to build the documentation check into the workflow before the writeup, not after lesson noted for next time.
If the kernel is behaving correctly, why was my laptop cooking itself?
Because min_perf_pct=100 got set on a laptop, and that setting isn't meant for laptops. It came from the tuned throughput-performance profile, which is applied at boot on this machine. Its own upstream description is blunt about the target audience:
Broadly applicable tuning that provides excellent performance across a variety of common server workloads
Server workloads. Sustained, predictable load, and critically server-class cooling and power budgets. min_perf_pct=100 on a machine with a 15 W package power limit is a fine choice on a rack server with real thermal headroom. On a Broadwell-U ultrabook with a 15 W RAPL PL1, it just means the CPU spends its entire life fighting the power limiter instead of scaling down when idle.
The real bug: if you want to call it that is a profile-selection/config mismatch: a server-oriented tuned profile getting applied to thermally-constrained mobile hardware, with nothing in the middle catching that mismatch or warning about it.
Two parts: get off the pinned floor right now, and make sure it doesn't come back after the next tuned-adm profile reapply or reboot.
Lower the global floor and switch governors:
$ echo 27 > /sys/devices/system/cpu/intel_pstate/min_perf_pct
$ echo schedutil | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
Full AFTER log: governor switch, min_perf_pct steps, verification
--- governor switch alone is NOT enough (QoS pin persists) ---
echo schedutil > scaling_governor (all 4 cpus)
governor = schedutil BUT scaling_min_freq = 2900000, min_perf_pct = 100 (still pinned)
--- scaling_min_freq write still ineffective at min_perf_pct=100 (confirmed again here) ---
--- lowering min_perf_pct releases the floor ---
echo 27 > /sys/devices/system/cpu/intel_pstate/min_perf_pct
=> min_perf_pct = 27, scaling_min_freq = 783000, scaling_cur_freq = 2693 MHz (scaling with load)
echo 50 > /sys/devices/system/cpu/intel_pstate/min_perf_pct (verification, stays stable)
=> min_perf_pct = 50 (unchanged after 3s), scaling_min_freq = 1450000
turbostat, "fixed" phase (~36–38% busy):
878 36.47 2412 2295 1.12 10533 0 0 154694 66.09 0.00 0.17 0.71 9.12 6.13 26.03
2.04 17.61 1.14 21.91 8.36 5.82 27.44 81 0 81 77.15 300 300 3.71 2.29 1.71
5.69 0.00 0.00 0.00 6.84 5.29 0.06 1.52
769 37.42 2060 2295 1.19 8937 0 0 148147 67.49 0.00 0.23 0.47 6.50 4.19 28.82
1.72 18.46 2.00 20.90 6.29 3.98 31.40 80 0 81 79.04 300 300 4.06 1.85 1.02
7.10 0.00 0.00 0.00 5.62 4.05 0.05 1.48
967 37.84 2562 2294 1.12 10646 0 0 178202 68.13 0.01 0.16 0.86 10.04 6.96 26.78
1.77 13.40 1.49 21.81 9.12 6.41 24.82 90 0 90 78.24 300 300 3.66 2.65 1.87
5.37 0.00 0.00 0.00 7.52 5.97 0.05 1.56
Bzy_MHz now varies 1988–2562 depending on load, instead of sitting locked at 2795–2807.
Throughput check (2-core burn, 3s), to make sure the fix wasn't costing performance:
under load : cpu MHz = 2494 / 2494 / 2530 / 2530 (RAPL-capped identical to before the fix)
after drop : cpu MHz = 2693 / 2693 / 2693 / 2693 (decaying, not pinned)
No throughput loss. Makes sense sustained load was always RAPL-capped regardless of the floor; the floor only mattered when the machine was not under load, and that's exactly the case it was hurting.
Make it stick. Reapplying the throughput-performance profile would just set min_perf_pct=100 again, so I created a local override instead of hand-patching global sysfs state every boot:
$ mkdir -p /etc/tuned/profiles/throughput-performance
$ cat /etc/tuned/profiles/throughput-performance/tuned.conf
# governor=schedutil; min_perf_pct=100 line removed; rest identical to distro file
$ tuned-adm profile throughput-performance
/etc/tuned/profiles/ takes precedence over the shipped profile of the same name, so this survives tuned updates without losing the override.
Final verification snapshot (per-CPU, post-fix, post-reapply)
intel_pstate: status=passive no_turbo=0 max_perf_pct=100 min_perf_pct=17 num_pstates=25 turbo_pct=25
cpu0: driver=intel_cpufreq gov=schedutil min=500000 max=2900000
cpu1: driver=intel_cpufreq gov=schedutil min=500000 max=2900000
cpu2: driver=intel_cpufreq gov=schedutil min=500000 max=2900000
cpu3: driver=intel_cpufreq gov=schedutil min=500000 max=2900000
turbostat (busy ~40-54%, background session still running):
1397 51.13 2732 2300 1.06 14013 0 0 204633 67.36 0.01 0.18 1.18 12.79 8.29 22.03
0.79 2.78 0.09 19.91 9.67 6.57 12.72 102 10 102 69.28 300 300 7.80 0.36 0.43
0.00 0.00 0.00 0.00 9.82 8.05 0.08 1.72
1473 54.21 2719 2298 1.04 13141 2 0 217627 67.77 0.02 0.21 1.13 11.14 7.17 20.22
0.80 4.07 0.32 19.91 8.38 5.25 12.26 90 23 90 64.23 300 300 5.16 0.28 0.50
0.17 0.00 0.00 0.00 10.19 8.41 0.09 1.78
1049 40.76 2584 2291 1.14 10269 0 0 194151 70.24 0.01 0.27 0.73 8.67 6.17 28.57
1.68 11.74 0.73 19.87 7.68 5.54 26.15 94 0 94 77.78 300 300 4.15 2.32 1.57
6.82 0.00 0.00 0.00 7.64 6.12 0.05 1.56
thermal_throttle (cumulative; still incrementing under sustained load, which is expected
RAPL capping under real load is normal. The pin's actual harm was idle/light-load
throttling, which is what's gone now):
cpu0: core=137776 pkg=137848 | cpu1: core=137776 pkg=137848 | cpu2: core=65 pkg=137852 | cpu3: core=65 pkg=137850
Before (min_perf_pct=100) |
After (min_perf_pct=17) |
|
|---|---|---|
scaling_min_freq |
2900000 (max turbo) | 500000 (min P-state) |
min_perf_pct |
100 | 17 |
| governor | performance | schedutil |
| Bzy_MHz, light load | constant 2795–2807 | 1988–2562, scales with load |
| idle-capable floor | none — 2.9 GHz min | 500 MHz |
| throughput under load | ~2.2–2.5 GHz (RAPL-capped) | ~2.2–2.5 GHz (identical) |
| core temp, ~23% busy | 83–88°C | n/a at matching load machine was busier during after-measurement, but idle/light throttling from the pin is gone |
| power-limit throttles | 115,804 pkg / 2h | no longer forced at idle/light load |
Same throughput ceiling under real load, meaningfully less heat and power draw everywhere else, and a laptop that actually behaves like a laptop again.
If you're on Broadwell-U-class (or similarly TDP-constrained) Intel hardware, intel_pstate in passive mode, and your scaling_min_freq writes seem to silently do nothing:
/sys/devices/system/cpu/intel_pstate/min_perf_pct first. If it's near 100, that's very likely your actual floor scaling_min_freq can't go below whatever the global knob demands, by design.tuned, but it could be a distro default, a power-profile daemon, or a manual tweak you forgot about.min_perf_pct is what actually moves the floor. scaling_min_freq will follow it down.tuned's throughput-performance (or anything built on it, the MSSQL, virtual-host, or HFT-adjacent profiles inherit the same setting), remember it was designed for servers. On mobile/thermally-constrained hardware it's very likely the wrong default, and a local profile override under /etc/tuned/profiles/<name>/ is the clean way to fix it without patching the distro package.
## Where I landedNo kernel patch out of this one the code's doing what it's documented to do, and I don't think there's a functional bug here to fix. Two things I do think are worth following up on, separately:
intel_pstate.rst making the "a near-100 global min_perf_pct will silently swallow your scaling_min_freq write" interaction more explicit and example-driven, since it took me a full read of the coordination-rules section (and an embarrassing near-miss on a bug report) to actually internalize it.tuned project about whether throughput-performance should carry a louder warning or a hardware-class check before it gets auto-applied on battery-powered, low-TDP systems.
Neither of those is glamorous. But "I found something that looked exactly like a bug, dug in, and it turned out the system was working as designed here's the real root cause instead" is, I'd argue, a more useful thing to publish than another closed WORKSFORME ticket would have been.If you've hit something similar on other Intel mobile chips or other tuned profiles, I'd be curious to compare notes in the comments.