Power Management

Revision as of 22:50, 16 January 2026 by LordXerus (talk | contribs) (change lang=sh to shell session to fix syntax highlighting error)

This article covers configurations related to power management in terms of energy saving modes of various devices and components.

Configuration

Hard drives

Following snippet configures Udev rules which automatically run the program hdparm to enable power saving modes for hard disks, especially rotational drives mapped to /dev/sd*.

services.udev.extraRules = 
  let
    mkRule = as: lib.concatStringsSep ", " as;
    mkRules = rs: lib.concatStringsSep "\n" rs;
  in mkRules ([( mkRule [
    ''ACTION=="add|change"''
    ''SUBSYSTEM=="block"''
    ''KERNEL=="sd[a-z]"''
    ''ATTR{queue/rotational}=="1"''
    ''RUN+="${pkgs.hdparm}/bin/hdparm -B 90 -S 41 /dev/%k"''
  ])]);

The hdparm parameters -B and -S define power saving modes and in case of -S the standby (spindown) timeout. The number 41 means therefore: Turn off the motor after 205 = 41*5 seconds.

Suspend hooks

NixOS provides the powerManagement.resumeCommands option which defines commands that are added to a global script that will be executed after resuming.

powerManagement.resumeCommands = ''
  echo "This should show up in the journal after resuming."
'';

It is also possible to use the post-resume target directly to make a service.

  systemd.services.your-service-name = { 
    description = "Service description here";
    wantedBy = [ "post-resume.target" ];
    after = [ "post-resume.target" ];
    script = ''
    echo "This should show up in the journal after resuming."
    '';
    serviceConfig.Type = "oneshot";
  };

Hibernation

Hibernation requires a configured swap device. See installation instructions on how to create a swap partition.

Please note that resumeDevice must match the output of swapon -s especially if you're dealing with mapped volumes (LUKS, logical volumes, logical volumes under LUKS, etc.). If you're using a swapfile, you must also specify the offset to it.

Therefore, an example configuration could look like this:

/*
  I'm hibernating into a logical volume that's also under LUKS. Pretty cool, right?
*/

swapDevices = [
  {
    device = "/dev/VG/SWAP";
  }
];

boot.resumeDevice = "/dev/dm-7";

Derived from a system with the following output from swapon -s :

Filename                                Type            Size           Used             Priority
/dev/dm-7                               partition       67108860       00
/dev/zram0                              partition       32881148       032767

Test and use hibernation with the following command:

systemctl hibernate

Tips and tricks

Go into hibernate after specific suspend time

Using following configuration, your system will go from suspend into hibernate after 1 hour:

systemd.sleep.extraConfig = ''
  HibernateDelaySec=1h
'';

Or, to disable suspend entirely, consider a configuration like this:

systemd.sleep.extraConfig = ''
  AllowSuspend=no
  AllowHibernation=no
  AllowHybridSleep=no
  AllowSuspendThenHibernate=no
'';

Troubleshooting

System immediately wakes up from suspend

Particularly in some Gigabyte motherboards with NVMe drives, the system may immediately wake up from being suspended. This can be worked around by disabling the wakeup triggers for the offending components:

Solution 1: Disabling wakeup triggers for all PCIe devices

If you don't need your system to wakeup via PCIe components you can simply disable it for all without needing to determine which component is causing problems.

services.udev.extraRules = ''
  ACTION=="add", SUBSYSTEM=="pci", DRIVER=="pcieport", ATTR{power/wakeup}="disabled"
'';

Solution 2: Disable a common NVMe interface

Specifically on Gigabyte motherboards you can try targetting only the NVMe ports.

services.udev.extraRules = ''
  ACTION=="add" SUBSYSTEM=="pci" ATTR{vendor}=="0x1022" ATTR{device}=="0x1483" ATTR{power/wakeup}="disabled"
'';

Solution 3: Disable a single device's wakeup triggers

If you wish to be more granular in what components should no longer be able to wakeup your system, you can find out which component is causing the wakeup events.

First, list all components and their current wakeup status:

$ cat /proc/acpi/wakeup
Device	S-state	  Status   Sysfs node
GP12	  S4	*enabled   pci:0000:00:07.1
GP13	  S4	*disabled  pci:0000:00:08.1
XHC0	  S4	*enabled   pci:0000:0a:00.3
GP30	  S4	*disabled
....
PT27	  S4	*disabled
PT28	  S4	*disabled
PT29	  S4	*disabled  pci:0000:03:09.0

You can temporarily toggle a device by writing its "Device" name back into /proc/acpi/wakeup

echo GPP0 | sudo tee /proc/acpi/wakeup

After finding out which component is causing unwanted wakeups you can use the sysfs id to find out the "vendor" and "device" fields:

$ cat /sys/class/pci_bus/0000:04/device/0000:04:00.0/vendor
0x1987
$ cat /sys/class/pci_bus/0000:04/device/0000:04:00.0/device
0x5013

And finally use those values in a udev rule:

services.udev.extraRules = ''
  ACTION=="add" SUBSYSTEM=="pci" ATTR{vendor}=="0x1987" ATTR{device}=="0x5013" ATTR{power/wakeup}="disabled"
'';

Suspend blocked by pre-sleep.service

Sometimes, the system appears to suspend (Wi-Fi turns off, screen locks), but the hardware does not actually suspend, and all subsequent systemctl suspend or systemctl reboot commands are met with:

# systemctl suspend
Call to Suspend failed: Action suspend already in progress, refusing requested suspend operation.

# systemctl reboot
Call to Reboot failed: Action suspend already in progress, refusing requested reboot operation.

If directly telling the kernel to suspend as root works:

# echo mem > /sys/power/state

Then a long-running pre-sleep.service might be hanging the sleep. This can be verified with systemctl list-jobs:

# systemctl list-jobs 
JOB   UNIT                    TYPE  STATE  
12144 suspend.target          start waiting
12149 pre-sleep.service       start running
12145 systemd-suspend.service start waiting
12268 post-resume.target      start waiting
12148 sleep.target            start waiting
12269 post-resume.service     start waiting

Here, the pre-sleep.service is blocking and halting suspend. To see why, we can use systemctl cat pre-sleep.service:

# systemctl cat pre-sleep.service
# /etc/systemd/system/pre-sleep.service
[Unit]
Before=sleep.target
Description=Pre-Sleep Actions

[Service]
# <... Omitted Environment directives PATH, LOCALE_ARCHIVE, TZDIR ...>
ExecStart=/nix/store/yzf7cpiqzq49san2frijxsh160zjy6fp-unit-script-pre-sleep-start/bin/pre-sleep-start 
Type=oneshot

[Install]
WantedBy=sleep.target

The pre-sleep-start script referenced by ExecStart contained directives installed by the Displaylink package, that contained a flush operation which hung the suspend action. Starting dlm.service or running sudo DisplayLinkManager unblocks the script and made suspend work normally.

An existing suspend operation that is hung may be interrupted using systemctl cancel in case reboots or internet access is needed.

See also

External resources