My Laptop's CPU stuck at Max Turbo 24/7 [ Here's Why ]

My Laptop's CPU stuck at Max Turbo 24/7 [ Here's Why ]

# linux# performance# debugging
My Laptop's CPU stuck at Max Turbo 24/7 [ Here's Why ]Muhammad Bilal

TL;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.

The symptom

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The min_perf_pct = 100 line is the one that matters, and I'll come back to it.

turbostat doesn't lie

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

First theory: the driver is silently eating my write

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
Enter fullscreen mode Exit fullscreen mode

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.

Down into intel_cpufreq_cpu_init()

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;
Enter fullscreen mode Exit fullscreen mode

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.

Writing the bug report (before I caught myself)

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's FREQ_QOS_MIN request, derived from the global min_perf_pct knob, silently overrides the user's scaling_min_freq write and pins the CPU at max turbo frequency until min_perf_pct is 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.

So what's actually wrong here?

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.

The fix

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

/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
Enter fullscreen mode Exit fullscreen mode

Before vs. after

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.

What I'd tell someone else hitting this

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:

  1. Check /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.
  2. Check what set it. For me it was tuned, but it could be a distro default, a power-profile daemon, or a manual tweak you forgot about.
  3. Lowering min_perf_pct is what actually moves the floor. scaling_min_freq will follow it down.
  4. If you're on 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 landed

No 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:

  • A short documentation patch to 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.
  • A note to the 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.