Why macOS Power Management Differs from Linux

On Linux, sleep inhibition goes through systemd-inhibit or directly writing to /sys/power/inhibit. On macOS, the kernel's IOPMLib handles power assertions, and the two terminal interfaces into that system are caffeinate (user-space assertions) and pmset (persistent policy via the powerd daemon).

MacOS 15 ships powerd at /usr/libexec/powerd. You cannot replace it, but you can query its current state with pmset -g assertions at any time. This command shows every active power assertion on the system, including those from GUI apps like Zoom or Time Machine. If your script or CI job fails to sleep when expected, run pmset -g assertions first before assuming your configuration is wrong.

Hibernation on macOS also works differently than on Linux. There is no swap partition. macOS writes a sleepimage file to disk at /private/var/vm/sleepimage. On Apple Silicon Macs, the hibernation model changed again - the unified memory architecture means sleep state is handled partly in the Secure Enclave, and some pmset hibernation flags behave differently than on Intel. We note specific differences throughout.

pmset -g assertions

caffeinate: Preventing Sleep During Long Jobs

caffeinate creates a power assertion that prevents the system from sleeping while a command runs. The basic syntax is caffeinate -s for system sleep prevention, but the flag you need depends on what you actually want to prevent.

Flags that matter in practice: - -i prevents idle sleep (the system goes idle but does not sleep) - -s prevents system sleep entirely, but only works when on AC power - -d prevents the display from sleeping - -m prevents the disk from sleeping - -u declares that a user is active, useful in scripts that need to simulate user presence - -t sets a timeout in seconds - -w waits for a process by PID to exit, then releases the assertion

For CI runners and build servers, the most useful pattern is wrapping a command directly. caffeinate -s runs the given command and holds the assertion until the command exits. On a GitHub Actions runner running on a Mac Mini, we use this pattern to prevent the machine from sleeping mid-build.

# Prevent sleep for the duration of a build
caffeinate -s ./build.sh

# Prevent sleep for exactly 2 hours (7200 seconds)
caffeinate -s -t 7200

# Attach to an already-running process by PID
caffeinate -w 48291

# Check what caffeinate is asserting (from another terminal)
pmset -g assertions | grep -i caffeinate

caffeinate in Scripts and Launchd Jobs

When caffeinate wraps a script, its exit code is the exit code of the wrapped command. This makes it safe to use in CI pipelines without masking failures. One gotcha: if you run caffeinate -s on battery, the -s flag silently degrades. It does not error. It simply does not prevent system sleep. Use -i instead if your machines may be unplugged.

For launchd jobs, you can add caffeinate as a wrapper in the ProgramArguments key. This is cleaner than modifying the script itself, because you can adjust the power assertion without touching your application code.





  Label
  org.myunix.nightly-backup
  ProgramArguments
  
    /usr/bin/caffeinate
    -s
    /usr/local/bin/backup.sh
  
  StartCalendarInterval
  
    Hour
    2
    Minute
    0
  

// advertisement

pmset: Reading the Current Power Policy

pmset -g shows the current power management settings. The output differs between battery and AC profiles. On a MacBook Pro M3 running macOS 15, a typical pmset -g output includes displaysleep, disksleep, sleep, womp (wake on network access), acwake, and hibernatemode among others.

Always read the current state before changing anything. On a fleet of managed Macs, unexpected pmset configurations are common because MDM profiles can set values that conflict with what you set manually. The command pmset -g custom shows both battery and AC profiles separately, which is more useful than pmset -g when debugging per-profile issues.

# Show all current power settings
pmset -g

# Show battery vs AC profiles separately
pmset -g custom

# Show scheduled wake/sleep events
pmset -g sched

# Show power source (battery, AC, UPS)
pmset -g ps

# Show sleep/wake history (last 21 events)
pmset -g log | tail -40

pmset: Writing Power Policy

pmset requires root to write settings. The -a flag applies to all power sources, -b to battery, -c to AC/charger, -u to UPS. Most server configurations only care about -c since rack Macs should always be on AC.

The sleep value is minutes of inactivity before sleep. Setting it to 0 disables sleep entirely on that profile. displaysleep controls the display separately. disksleep controls disk spindown for traditional HDDs - on Apple Silicon with internal NVMe this has no effect, but external drives may respond to it.

hibernatemode has three meaningful values on Intel Macs: 0 (no hibernation, RAM stays powered during sleep), 3 (safe sleep - RAM stays powered but a sleepimage is also written), and 25 (hibernate only - RAM is not powered, full disk write required). On Apple Silicon, hibernatemode 0 and 3 are effectively identical due to the architecture. Mode 25 is not supported on Apple Silicon.

For a headless Mac Mini being used as a build server, we use the following configuration. This disables sleep and display sleep on AC, keeps the disk active, and enables wake on network access so the machine responds to SSH after a power cycle.

# Disable sleep and display sleep on AC power
sudo pmset -c sleep 0 displaysleep 0

# Enable wake on network access (Magic Packet / SSH)
sudo pmset -c womp 1

# Disable disk sleep on AC
sudo pmset -c disksleep 0

# On Intel: disable hibernation for faster sleep/wake
sudo pmset -c hibernatemode 0

# Verify changes took effect
pmset -g custom

Wake Scheduling with pmset

pmset can schedule wake and sleep events by time. This is useful for machines that should power up before a scheduled job, or sleep during off-hours to save power. The schedule subcommand accepts wake, sleep, poweron, shutdown, and wakeorpoweron as event types.

wakeorpoweron is the most reliable for servers - it wakes a sleeping machine or powers on a halted one, covering the case where the machine was shut down due to a power event. Times are specified in MM/DD/YY HH:MM:SS format, which is unfortunate but fixed.

For repeating schedules, use weekday, weekend, or specific day names. We use this on a CI Mac Mini to ensure it wakes at 06:00 on weekdays before the first developer pushes code.

Scheduled events survive reboots. Check them with pmset -g sched and cancel a specific event with pmset schedule cancel followed by the event details. Canceling requires matching the exact event string, which is annoying - easier to cancel all events and re-add them.

# Wake every weekday at 06:00
sudo pmset repeat wakeorpoweron MTWRF 06:00:00

# Sleep every day at 22:00
sudo pmset repeat sleep MTWRFSU 22:00:00

# Schedule a one-time wake
sudo pmset schedule wake "08/21/26 07:30:00"

# List all scheduled events
pmset -g sched

# Cancel all repeating events
sudo pmset repeat cancel
// advertisement

hibernatefile and the sleepimage

By default, macOS writes the sleepimage to /private/var/vm/sleepimage. On a machine with 64GB RAM, this file is 64GB. On SSDs with limited write endurance under heavy sleep/wake cycles, or on machines where disk space is tight, this matters.

You can point hibernatefile at a different path or a different volume. On a Mac with a secondary internal SSD or a mounted APFS volume, this moves the write pressure. The path must be on a local filesystem, not a network mount.

On machines where hibernation is disabled (hibernatemode 0 on Intel, or Apple Silicon where you accept the behavior), the sleepimage still exists on disk but is not written during sleep. You can safely delete it after setting hibernatemode 0 and reclaim the space. It will not be recreated until the hibernation mode is changed back.

On Apple Silicon Macs, the sleepimage is typically much smaller than RAM size because the memory compression and Secure Enclave handling reduce what needs to be written. A 16GB M3 MacBook Pro's sleepimage is often under 4GB.

# Check current sleepimage path and size
pmset -g | grep hibernatefile
ls -lh /private/var/vm/sleepimage

# Move sleepimage to alternate path (Intel, hibernatemode 3)
sudo pmset -a hibernatefile /Volumes/FastSSD/sleepimage

# Disable hibernation and delete the sleepimage (Intel only)
sudo pmset -a hibernatemode 0
sudo rm /private/var/vm/sleepimage

Monitoring Power Events and Debugging Sleep Problems

pmset -g log is the primary debugging tool for sleep and wake issues. It shows the last several hundred power events with timestamps and reasons. The output includes wake reason codes that identify what caused the machine to wake - network (womp), user input (hid), scheduled event (sched), and others.

When a machine wakes unexpectedly, the wake reason field tells you what triggered it. A common culprit is mDNSResponder or sharingd holding a network power assertion. Another is a poorly configured UPS that sends spurious wake signals.

For real-time monitoring during debugging, syslog -F raw | grep -i sleep gives a live stream of sleep-related kernel messages. On macOS 15, log stream --predicate 'eventMessage contains "sleep"' gives structured output that is easier to filter.

If you are running automated infrastructure or DevOps pipelines on macOS - for example, Xcode build farms or macOS-hosted automation - power management bugs are a genuine source of flakiness. Tools like those at taskbotshub.ai address Mac fleet automation and can incorporate power state checks into job orchestration logic, which is more reliable than hoping pmset settings persist through MDM profile updates.

# Show recent sleep/wake log
pmset -g log | grep -E '(Sleep|Wake|DarkWake)' | tail -30

# Live power event stream (macOS 15)
log stream --predicate 'eventMessage contains[c] "sleep" OR eventMessage contains[c] "wake"' --style syslog

# Show what prevented sleep last time
pmset -g log | grep -i 'Prevent'

# Show current active assertions with their owners
pmset -g assertions

UPS Integration and Power Failure Handling

macOS has native UPS support via IOKit. When a UPS is connected over USB, pmset sees it as a power source and exposes UPS-specific settings: haltlevel (battery percentage at which to shut down), haltafter (minutes after power loss at which to shut down), and haltatremaining (minutes of battery remaining at which to shut down).

For a UPS-backed Mac Mini serving as a local infrastructure node, setting a conservative haltlevel ensures a clean shutdown before the UPS battery dies. We set haltlevel to 15 and haltatremaining to 5 minutes on our test server, which gives enough time for a clean shutdown even if the power event happens at a bad moment.

Note that mdmclient and configuration profiles can override UPS settings. If your pmset -g shows values you did not set, check System Settings > Profiles or run profiles -P from the command line to see installed configuration profiles.

# Show UPS status
pmset -g ps

# Shut down when UPS battery reaches 15%
sudo pmset -u haltlevel 15

# Shut down 10 minutes after power loss
sudo pmset -u haltafter 10

# Shut down when 5 minutes of battery remain
sudo pmset -u haltatremaining 5

# List installed configuration profiles (may override pmset)
profiles -P
// advertisement

Scripting Power State Checks

For scripts that need to branch based on power state - for example, a backup script that should not run on battery - pmset -g ps provides parseable output. The IOKit power source type is listed as 'AC Power', 'Battery Power', or 'UPS Power'.

A cleaner approach on macOS 15 is using the pmset -g rawlog output or checking /Library/Preferences/SystemConfiguration/com.apple.PowerManagement.plist directly with plutil, but parsing pmset -g ps is simpler and more reliable across versions.

For fleet-level automation, generating a pmset configuration as part of a provisioning script ensures consistency. If you manage naming conventions for your Mac fleet - hostnames, project identifiers for launchd labels - a tool like nicename.me can help generate clean, consistent names that work well in launchd label strings and log filtering.

The script below is a minimal example of a power-aware wrapper that skips a resource-intensive job when on battery.

#!/bin/bash
# power-check.sh - Skip heavy jobs on battery power

POWER_SOURCE=$(pmset -g ps | head -1 | awk -F"'" '{print $2}')

if [[ "$POWER_SOURCE" == "Battery Power" ]]; then
  echo "On battery - skipping intensive job"
  exit 0
fi

echo "On AC power ($POWER_SOURCE) - proceeding"
caffeinate -s ./heavy-job.sh

# Log power state at job completion
echo "$(date): Job completed. Power assertions:"
pmset -g assertions | grep -v '^\.\|^$'

Resetting pmset to Defaults

If a pmset configuration gets into a broken state - mismatched settings, MDM conflicts, or leftover values from a previous admin - the reset subcommand restores factory defaults for all power settings on all profiles. This is a clean-slate operation.

After reset, reboot to ensure powerd picks up the defaults cleanly. Then re-apply your configuration from a provisioning script rather than applying settings incrementally, which makes the configuration auditable and reproducible.

On macOS 15, the defaults after reset on a MacBook Pro M3 are: sleep 1 minute on battery, 10 minutes on AC; displaysleep 2 minutes on battery, 10 minutes on AC; hibernatemode 3; womp 1 on AC; powernap 1 on AC.

# Reset all pmset settings to macOS defaults
sudo pmset -a restoredefaults

# Confirm reset (should show Apple defaults)
pmset -g custom

# Then reboot
sudo reboot