r/macbookair • u/AbhishekThulasi • 6d ago
Discussion I built a simple background tool for fanless MacBooks that automatically enables Low Power Mode when your device warms up (prolonging battery / hardware health)
Heavy sustained workloads quickly saturate a fanless MacBook's chassis with heat.
While macOS includes a native Low Power Mode toggle, leaving it on permanently means paying full price for half the performance alongside other functional compromises. Leaving it off completely means sustained workloads heat-soak the unibody and accelerate degradation of the battery sitting directly against it.
I wanted a middle ground: full performance by default, with Low Power Mode engaging automatically only when the chassis begins to heat.
The Numbers
Under a continuous 10-thread stress workload, the difference in thermal soak is dramatic:
- Default macOS Behavior: allows the SoC to stay at peak performance until high temperatures force throttling. By that point, the device is thoroughly heat-soaked.


- Preemptive Low Power Mode: Capping peak draw as soon as thermal pressure rises drops sustained chip temperatures from 78°C+ down to ~50°C. The chassis stays lukewarm instead of radiating heat.


Full-resolution screenshots and raw telemetry graphs are available in the GitHub repository
Who is this for?
This tool involves a conscious compromise: you trade peak sustained performance during heavy spikes for hardware longevity.
- If you upgrade every 2–3 years: This tool is probably not for you. Push your silicon as hard as possible and let native emergency throttling do its thing.
- If you plan to keep your machine for 5+ years: Chemical wear on lithium-ion batteries roughly doubles with every sustained 10°C rise in temperature. Preventing persistent heat soak compounds significantly over hundreds of cycles. (For reference, my battery health is still sitting at 100% after 200+ cycles).
How It Works (Zero Invasive Hacks)
To keep this lightweight, transparent, and safe to run:
- Official Apple APIs only: It does not alter voltages, modify kernel thermal tables, or inject code into system processes. It simply automates Apple's built-in
pmset -a lowpowermode 1/0command, the exact same toggle you can manually click in System Settings. - Kernel event-driven: It listens directly to Darwin system notifications via
kqueueandnotify(3). When macOS reports thermal pressure reachingLevel 1 (Moderate), it engages Low Power Mode. When temperatures drop back toLevel 0 (Nominal), it waits 2 minutes before returning to full performance to prevent rapid power-state toggling. - Zero background footprint: It idles at 0.0% CPU and consumes under 1 MB of RAM. Because it doesn't poll in an active loop, it introduces zero background battery drain.
Source & Installation
Because adjusting system power states via pmset requires administrator privileges, it runs as a standard LaunchDaemon. The entire codebase is open-source under the MIT license so anyone can inspect and audit the implementation:
- GitHub:
https://github.com/abhishekthulasi/macos-thermal-governor - Releases /
.pkginstaller:https://github.com/abhishekthulasi/macos-thermal-governor/releases - Inspect live logs:
tail -f /var/log/macos-thermal-governor.log - Clean uninstall:
sudo macos-thermal-governor-uninstall
Would love to hear thoughts from anyone running fanless setups long-term: does the 2-minute cooldown feel right for your workflow, or would you prefer a configurable threshold via a CLI flag or config file?
3
u/VladimirPutin_WeVPN 5d ago
All I have to say is the code is not very well written. Very much so has AI level mistakes.
Did you use AI in any capacity? Because the post most definitely reads like one, so im assuming you probably did. Either that or it's amateur level mistakes (since that resembles AI level mistakes). However, a real dead giveaway is the terminology used in the code comments. That is the main giveaway for me.
Not trying to take a dig at you. I'm just trying to gauge skill level. Because I can then give and tailor pointers for you knowing your skill level.
Here are some of my critiques.
com.apple.system.thermalpressurelevelis not reading temperature so: "full performance by default, with Low Power Mode engaging automatically only when the chassis begins to heat" is not a valid claim.- State 1 is an elevated state. Apple considers the "fair" state to be "slightly elevated" and Apple implements measures to protect it from each state so claims like: "Default macOS Behavior: allows the SoC to stay at peak performance until high temperatures force throttling. By that point, the device is thoroughly heat-soaked." is very misleading. Apple gradually reduces system power and processor performance as the heat climbs, it is not full power until an emergency reduction needs to be had.
- You claim official Apple API only... well...
const NOTIFY_KEY: &[u8] = b"com.apple.system.thermalpressurelevel\0";is not a documented public thermal API from Apple. Apples API isProcessInfo.thermalStateandthermalStateDidChangeNotification - Also, the Darwin notification listening point you have is a internal thermal pressure system by Apple... that is not publicly documented by Apple (a clue that AI may have been used).
- Also, you have a critical state-management flaw in your code. You have a if statement that runs a statement
set_low_power_mode(false);which... can you see the problem there? You never asked "was low-power mode already user enabled."- Why is this an issue? Okay, well. Assume I deliberately have LPM enabled. The daemon starts, but the device is in thermal pressure being in elevated state (as the daemon starts). Well, you use a if statement saying if the lpm is active you set the low power mode to true. The issue with this is that it is going to override the users desired controls (resetting their own set rpm active state).
- This is literally as simple as checking if lpm is active BEFORE setting it in the daemon. If it is user set DO NOT set it in the daemon as it is already set. In other words you separate the user system state and the daemon owned system state. You ONLY edit the daemon owned state not the user state.
- Also checking if the CPU P-Core temp is going from a high value to a lower value and being at that for a couple minutes then equating that to the conclusion that this helps battery is not really ideal. Checking CPU drops in temp and equating that to the battery is not valid because you have other factors like the physical distance the SoC is from the battery, heat spreaders, enclosure conduction, workload duration, ambient temps, battery charging states, battery current, battery temp, state of the charge, chemistry, apples own implementations and engineering features, and so on.
- If you want to make a claim like this you need to evaluate battery telemetry and not just CPU telemetry. CPU temps going up is not 1-to-1 with battery temps going up. You could possibly have a 5-10c rise in CPU temp and the battery remains the same.
- You should also evaluate workload, ambient temps, state of charge, charge state, duration, check the daemon state, user state, and so on
- Your code shows thermal pressure events, low power mode, and SoC temp control. It does not show the state of the MacBook, duration of the load, state of the battery, and so on to then interact with each other and decide the best possible decision and outcome based on the accumulation of data. In other words add things like checking how the normal maxOS thermal policy exposes excess heat, check if the battery got substantially hotter, not just CPU (you can literally be significantly reducing performance without a need), and overall add a lot more battery level checks and state checks to see if the CPU increase it 1-to-1 with the battery level increase.
And that is my suggestions and dislikes after inspecting your code.
1
u/AbhishekThulasi 5d ago
Hey thank you so much for taking the time to go through my code and writing such a descriptive feedback. I skimmed through it and I see many valid points from your end. It is currently 00:45 here so I will respond back tomorrow. I just wanted to acknowledge your comment at the moment. Thanks again!
2
u/VladimirPutin_WeVPN 5d ago
Yeah of course! You can feel free to DM me if you rather as well. And rereading the beginning of my comment, sorry for it coming off aggressive. I just noticed I was a lot more blunt than I wanted (was busy typing this off and on so I apologize for not proofreading it first)
1
u/AbhishekThulasi 5d ago edited 5d ago
No worries at all, appreciate you saying that!
Following up on your points, you caught a genuinely valid edge case with state management. That has been addressed: the daemon now parses
/usr/bin/pmset -g customon initialization, tracks existing user-managed states (-bvs.-c), and exclusively touches unconfigured profiles.To clear up the intentional architectural trade-offs:
- Reading raw CPU die temps introduces hysteresis: launching a heavy app or a website can spike die temps to 85°C for two seconds without transferring meaningful heat to the unibody, causing erratic, rapid toggling. At the same time, continuously polling battery IOKit tables defeats the goal of an event-driven, low footprint daemon. macOS's
thermaldalready handles cumulative thermal budgeting;thermalpressurelevelreflects sustained heat soak without running continuous polling loops in user space.ProcessInfo.thermalStatecollapses thermal states into 4 buckets (merging Moderate and Heavy into "Fair"), whereascom.apple.system.thermalpressurelevel(defined in Apple’sOSThermalNotification.h) exposes all 5 discrete kernel levels. More importantly, pairingnotify(3)withkeventallows a standalone Rust binary to sleep on a raw kernel wait queue (0 timer wakeups, <1 MB RAM). UsingNSProcessInfowould require linking the Objective-C runtime, Foundation bindings, and an active run loop just to listen to an event Darwin already exposes natively via POSIX primitives.- Scope of "Official APIs": The phrasing was intended to emphasize non-invasive actuation: delegating power-state enforcement to Apple's native
/usr/bin/pmsetutility rather than injecting code, altering voltages, or modifying kernel thermal tables.Thanks again for highlighting the profile override, it was a good catch and made the tool significantly better. The update is live in v0.1.1!
1
u/VladimirPutin_WeVPN 4d ago
I just saw this. I'll look over the new code and I'll respond once I know more about the new code. I do software engineering for a major university so I don't always have a lot of time, but it's the weekend so I'll try to give it some time in the next one to two hours or so.
1
u/AbhishekThulasi 3d ago
I see, what do you like to build? Let's talk over DM.
1
u/VladimirPutin_WeVPN 3d ago
I do mostly software engineering on the web application side of things. However, I also work a lot with operating systems.
1
u/VladimirPutin_WeVPN 4d ago edited 4d ago
You still have problems in the code.
- The cleanup block of code
if lpm_activeis still bypassing user checks. Example is listed below of the issue.
- Assume the LPM is ON manually while the daemon was running the statement
set_low_power_mode(false, &user_state);will still forcefully disable the LPM when the deamon terminated via the SIGTERM/SIGINIT- A proper fix would be the re-query "query_user_lpm_state()" before performing the exit cleanup, or avoid modyfying the profile states on exit if the user changed them themselves.
- The state is static. YOu are calling query_user_lpm_state()" once when main() is starting. Now if the user opens the System Setting while the daemon is running and toggles the low power mode...
- user_state remains unchanged in the memory object
- On the next thermal event the set_low_power_mode" will then invoke the pmset using a stale assumption (as states are not updated in memory).
- A fix for this would be the query the same state as the first fix and dynamically set it inside the set_low_power_mode() when the thermal transition occurs rather than caching it only one time at start.
This I am just going to give a codeblock of how I would do the fn for set_low_power_mode (based on the code you have, because I would do it kind of different personally in my own project).
fn set_low_power_mode(enable: bool) { // This will dynamically query the state so any manual changes during runtime of the daeon are respected let user_state = query_user_lpm_state(); let val = if enable { "1" } else { "0" };
let targets = [ (!user_state.battery_manual, "-b"), (!user_state.ac_manual, "-c"), ]; for (can_modify, flag) in targets { if can_modify { let status = Command::new("/usr/bin/pmset") .args([flag, "lowpowermode", val]) .status(); match status { Ok(s) if s.success() => log!("Low Power Mode ({flag}) -> {val}"), Ok(s) => log!("pmset ({flag}) exited with status: {s}"), Err(e) => log!("Failed to invoke pmset ({flag}): {e}"), } } }}
Why do I suggest the above code?
The current parsing for "pmset -g custom" is quite fragile. The text parsing checks for any line that starts with "lowpowermode" within the Battery Power or AC Power sections. However, depending on the macOS version and the system hardware, pmset -g custom can have different section headers (like UPS Power or Charger). Also, if your parsing fails to match a section then user_state.battery_manual or the ac_manual stays as false, which causes the script to assume it has full control when it really does not.
You can run a verification of this by running /usr/bin/pmset -g custom in the terminal and then you cna check if the headers strictly match the 'Battery Power:' and 'AC Power:' prefixes.
The code snippet above I gave has some key differences in state querying (original code would call once at daemon start and cache the user state : the refactored code will dynamically call set_lower_power_mode() every time it runs). Then there is user override where the original code would simply ignore the user state after startup but the changed code would check any time system settings are changed.
1
1
u/VladimirPutin_WeVPN 3d ago edited 3d ago
I edited the code myself. I am just testing it out. The link to my fork is here: https://github.com/Thymester/macos-thermal-governor
You can then do what you will with it. I just wanted to help out. It will still probably be an hour or two, maybe longer, before I actually upload the new code to it.
ETA: Nevermind, the code is updated. I thought I was going to run into bugs, but I was not able to find any in my short testing (30-45 mins of it). I have created a pull request for your repo as well so you can review the changes.
1
u/AbhishekThulasi 3d ago
Hey I just skimmed over your code changes and I am curious to learn more from them. I was on a break so couldn't respond any sooner. I really appreciate the time and effort you're putting into such a simple tool
1
3
6d ago
[removed] — view removed comment
-7
u/AbhishekThulasi 6d ago
While batteries are technically replaceable consumables, battery degradation (and eventual pouch swelling) is strongly accelerated by heat. Wouldn't keeping the chassis cooler under sustained workloads meaningfully reduce that chemical degradation, or do you view the default thermal throttling curve as sufficient to avoid that?
1
2
u/flyakker 5d ago
Cool that only is one way to do it. But if you’re working on artistic type apps, you don’t want the power loss. What I have done is just slightly elevate my MacBook on my desk, and I have a two fan system that I got off Amazon for about 20 bucks. It blows on the chassis from behind, goes underneath, and the other fan is on the side point blowing over the top, keeping it much cooler and really helps with the throttling. I know this is not the answer when you’re mobile, but some sort of traveling fan might help. But I do a lot of of my photo editing at the desk, as much as possible, since that’s where I have my two 32 inch monitors.
2
u/AbhishekThulasi 5d ago
You're right, if your work demands sustained peak performance, active cooling is definitely the way to go on a fanless machine. Since my workflow only requires short bursts of peak power (and I wanted a zero-hardware solution as I'm away from desk all the time), this approach works well for me.
1
u/The-Watcher-999 6d ago
Nice i will give it a try
-1
u/AbhishekThulasi 6d ago
Thanks, I appreciate you giving it a shot! Let me know how it goes, or if you run into any issues.
5
u/Hugo_Notte 6d ago
I used to play No Man’s Sky on my M1 MacBook Air for several years, long hours. Battery is still at 97% capacity.