ProcessInfo Thermal State on macOS: Adaptive Workloads Without Fan Guesswork
Use ProcessInfo thermal state to adapt macOS workloads safely, with notification ownership, hysteresis, testing, and measurable recovery.
macOS exposes a coarse system thermal state through Foundation’s ProcessInfo. It is intended to help applications reduce their own work as thermal conditions worsen. It is not a temperature sensor, a fan-speed API, a performance benchmark, or an explanation of which process caused the system to heat up. An application should treat the state as a platform signal for workload adaptation, not as permission to take control of cooling.
The practical goal is graceful degradation. A renderer may lower preview quality while preserving export quality. A synchronizer may postpone non-urgent indexing. A media application may reduce background analysis while keeping playback responsive. The correct response depends on the product’s promise and should be designed before a thermal event occurs.
Read the signal as a system-level category
ProcessInfo.processInfo.thermalState reports one of four categories: nominal, fair, serious, or critical. Apple describes nominal as within normal limits, fair as slightly elevated, serious as high, and critical as significantly affecting system performance while the device needs to cool. Those labels are intentionally broad. Do not convert them into degrees Celsius, infer a specific chip temperature, or claim that a particular CPU or GPU is throttling based only on this property.
The system may lower processor speed as heat rises, but a process cannot use this API to determine a single cause or accurately predict a frame rate. The state belongs to the system, not an individual task. Other applications and system services contribute to heat, and a laptop’s ambient temperature, enclosure, workload, and power source can all affect the result. Your feature should respond conservatively without claiming more diagnostic precision than the API provides.
Thermal state also is not equivalent to Low Power Mode, battery status, memory pressure, or a power assertion. Those signals can correlate but represent different constraints. If a feature already observes them, maintain separate inputs and avoid collapsing them into a single undocumented “slow device” boolean. A clear adaptation policy should explain which signal caused which change.
Build a small, reversible adaptation policy
Map thermal categories to product actions that reduce optional resource use. Useful actions include lowering live-preview frame rate, reducing nonessential animation, pausing speculative precomputation, limiting background concurrency, deferring batch work, and selecting a smaller intermediate cache. Keep interactive response and user data integrity ahead of cosmetic throughput. An export or scientific computation should not silently produce a lower-quality result just because a preview was reduced.
Start with no change at nominal. At fair, consider early low-cost reductions such as pausing speculative work. At serious, reduce optional concurrency and lower preview detail. At critical, stop or defer nonessential work and keep only operations required to preserve user data or maintain core interaction. These are examples, not universal prescriptions. A video editor, audio workstation, build system, and cloud-sync client have different acceptable degradations.
Make adaptation idempotent. Repeated notifications for the same state should not repeatedly allocate resources or compound a reduction. When the state improves, restore only settings that the thermal policy owns and that have not been changed by the user in the meantime. The user-selected quality setting remains authoritative; thermal adaptation should never make a user’s explicit choice impossible to recover.
Avoid rapid oscillation when conditions move around a boundary. A policy can apply a short dwell interval before restoring optional work, or require a stable transition before returning to a more expensive mode. Keep such hysteresis in the application’s scheduler rather than modifying the reported system state. Log state transitions and applied policy actions, not speculative sensor values.
Register and remove notification observers symmetrically
ProcessInfo.thermalStateDidChangeNotification reports state changes. Apple’s API documentation says to access thermalState before registering for this notification. Read an initial state, subscribe, and then reconcile the current state again after registration so the application does not miss a transition between the first read and observer setup. Use one owner for the observer token and remove it when that owner ends.
The notification’s object is a ProcessInfo instance. Scope the observer to the shared process information object when appropriate, rather than listening indiscriminately to notifications from unrelated objects. If notification handling updates UI, deliver the UI update on the main actor. If it changes a scheduler, serialize the policy transition so two concurrent notifications cannot apply out of order.
This example keeps the policy mapping pure and testable. It demonstrates querying the public state and deciding which optional workload tier to use; the application still needs an observer owner and product-specific scheduling code.
import Foundation
enum WorkloadTier: Equatable {
case full
case reduced
case minimal
}
func workloadTier(for state: ProcessInfo.ThermalState) -> WorkloadTier {
switch state {
case .nominal:
return .full
case .fair:
return .reduced
case .serious, .critical:
return .minimal
@unknown default:
return .reduced
}
}
let initialState = ProcessInfo.processInfo.thermalState
let initialTier = workloadTier(for: initialState)
print("Initial workload tier: \(initialTier)")
The unknown-case fallback is deliberately conservative: if a future operating system adds a state, the sample reduces optional work rather than assuming conditions are nominal. A shipped application should apply the chosen tier through a single coordinator and make the selected action observable to its tests and diagnostics.
Keep the policy reversible and explainable
The application should preserve a distinction between user preferences and temporary system adaptation. Store the user’s desired setting separately from the effective setting. For example, a user can request a high-quality preview while the current effective preview is temporarily reduced. When conditions improve, the app can restore the requested quality automatically without rewriting the preference.
Make the adaptation discoverable. A subtle status label or a short explanation can tell a user why a preview is temporarily less detailed. Do not display a scary generic alert every time a state changes. Critical interventions should be reserved for actual product impact, such as pausing an optional render queue or requiring the user to resume a background operation.
Never use thermal state as a reason to discard unsaved work. Persist checkpoints before starting large optional jobs, use bounded queues, and make cancellation cooperative. When a job is paused, record its inputs and progress in a recoverable form. If a task is not safe to pause, prioritize finishing the small atomic section already in progress and defer starting the next one.
Measure the effect instead of assuming it
Instrument the work your product controls: CPU time, GPU workload where available through supported tools, frame delivery, queue depth, memory, completion time, and effective quality. Correlate those measures with state transitions in a privacy-preserving way. This can reveal a faulty policy that reduces visual quality without reducing the expensive work, or one that worsens latency by repeatedly tearing down and rebuilding a pipeline.
Compare behavior in controlled runs with the same input and machine conditions. Record OS build, hardware class, power mode, external display configuration, and task duration. Thermal behavior is affected by environmental factors, so avoid claiming a causal improvement from one short run. Use sustained workloads and repeat trials.
Do not use a thermal state transition as a substitute for a performance diagnosis. A high CPU process, blocked I/O, an infinite loop, or an inefficient algorithm remains a defect even if the UI responds by lowering frame rate. Profiling tools and measurements are still needed to identify the underlying work.
Test transitions and lifecycle edge cases
Test the app’s behavior at every category, repeated notifications, transitions down and back up, and a state change while a long-running task is active. Unit-test the pure mapping and the coordinator’s idempotence. Integration-test the observer’s installation, removal, and delivery queue. Make sure closing a window does not leave a notification callback retaining its view controller or starting background work unexpectedly.
Apple provides guidance for testing under adverse device conditions in its developer tools. Use supported testing facilities and real hardware where necessary; do not rely on private APIs that force device temperature. If a development environment cannot reliably produce each thermal state, test the policy coordinator with injected state values and label actual thermal behavior as not exercised.
Exercise other signals independently. A battery or Low Power Mode transition must not be misreported as a thermal event. Memory pressure must follow its own recovery policy. A user toggling a quality preference while a thermal policy is active must not create a lost update. The final effective configuration should be deterministic and inspectable.
Operational checklist
Before release, identify which work is optional, define reversible tiers, subscribe only for the lifetime that needs adaptation, and verify teardown. Confirm that thermal response does not reduce saved-output quality silently, delete work, override user settings permanently, or make false claims about temperature. Add telemetry for adaptation decisions without collecting private workload content.
The thermal-state API gives macOS applications a useful coordination hint. Production behavior comes from a policy that is modest, reversible, measurable, and aligned with what the application promises. When an adaptation cannot be explained or safely undone, it should not be triggered automatically by a coarse system signal.
Related:
- macOS Power Assertions: Preventing Sleep Without Hiding the Reason
- Fixing macOS Memory Pressure and ‘Out of Application Memory’ Warnings
Sources: