Skip to content

Instantly share code, notes, and snippets.

@SuperMatt
Last active April 1, 2026 15:11
Show Gist options
  • Select an option

  • Save SuperMatt/4d59b94ec0263a88898e0fe034845989 to your computer and use it in GitHub Desktop.

Select an option

Save SuperMatt/4d59b94ec0263a88898e0fe034845989 to your computer and use it in GitHub Desktop.
Fix battery drain during suspend on Linux (Modern Standby/s2idle laptops) — suspend-then-hibernate with Btrfs swap file

Fix: Battery Drain During Suspend on Linux (Modern Standby / s2idle laptops)

The Problem

On many modern laptops (especially AMD-based systems from Dell, Lenovo, HP, and others), closing the lid suspends the laptop — but it still drains roughly 3% battery per hour, potentially dying overnight. This is caused by the suspend mode the hardware uses.

Why This Happens

Modern AMD laptops and many recent laptops have dropped S3 sleep ("Suspend to RAM") in favour of Modern Standby (s2idle). You can confirm this:

cat /sys/power/mem_sleep
# Output: [s2idle]   ← only s2idle listed; deep (S3) is not available

With S3, the CPU and nearly all hardware cut power entirely during suspend. With s2idle, the CPU stays in a low-power idle loop — much higher drain.

This is a hardware/firmware limitation driven by Microsoft's Modern Standby standard. Many vendors have removed S3 from their ACPI tables entirely. There is often no BIOS setting to change it.


The Fix: suspend-then-hibernate

Configure systemd to:

  1. Suspend normally when the lid closes (fast, instant wake for short absences)
  2. Automatically hibernate after 60 minutes of suspended sleep (writes RAM to disk, powers off completely — zero battery drain)

When you open the lid after hibernation, your full session restores exactly as you left it.


Tested Configurations

Field Fedora Ubuntu
Hardware Dell Inspiron 15 3525 Dell Inspiron 15 3525
CPU AMD Ryzen 7 5825U AMD Ryzen 7 5825U
OS Fedora 44 Ubuntu 26.04
Kernel 6.19.9-300.fc44.x86_64 7.0.0-10-generic
systemd 259 259
Filesystem Btrfs on NVMe ext4 on NVMe
Available sleep states freeze mem disk freeze mem disk

The steps below apply to both distros. Distro-specific sections are clearly marked.


Prerequisites

Hibernate requires writing your entire RAM to disk. Many systems use zram as their only swap device — this is compressed in-memory swap and cannot be used for hibernate. Check:

swapon --show
# If you only see /dev/zram0 (TYPE=partition, backed by RAM), you need a persistent swap file

You need a swap file on persistent storage at least as large as your RAM. Check RAM size:

free -h | grep Mem

The steps below create an 8 GB swap file — adjust to match your RAM in MB.


Apply: Full Setup

Step 1: Create a swap file at /swapfile

Btrfs — swap files must have copy-on-write disabled. Use dd (not fallocate — it doesn't work on Btrfs). The file must exist and be empty before setting chattr +C.

sudo touch /swapfile
sudo chattr +C /swapfile

# Fill with zeros — adjust count= to your RAM size in MB
sudo dd if=/dev/zero of=/swapfile bs=1M count=8192 status=progress

sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
swapon --show

ext4 / XFS — use fallocate instead:

sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
swapon --show

Step 2: Set the correct SELinux context — Fedora / RHEL only, skip on Ubuntu

On SELinux-enabled systems, files created at / get a generic label. The swap file needs the swapfile_t type or systemd-logind will be denied access and hibernate will fail with "Access denied".

sudo semanage fcontext -a -t swapfile_t "/swapfile"
sudo restorecon -v /swapfile

# Verify
ls -laZ /swapfile
# Should show: unconfined_u:object_r:swapfile_t:s0

Note: If you skip this step on a system with SELinux in Enforcing mode, systemctl hibernate will return "Access denied". Check sudo ausearch -m avc -ts recent if you hit unexplained access denials.

Ubuntu uses AppArmor (not SELinux) — skip this step entirely.

Step 3: Persist the swap file in /etc/fstab

Add this line to /etc/fstab:

/swapfile  none  swap  defaults  0 0

Step 4: Ensure the dracut resume module is included

Both Fedora and Ubuntu 26.04+ use dracut to build the initrd. Without the resume dracut module, the initrd will not run systemd-hibernate-resume.service on boot, leaving /sys/power/resume as 0:0 — and hibernate will fail with "Invalid resume config".

Check if it's already included:

# Fedora
sudo lsinitrd /boot/initramfs-$(uname -r).img | grep resume

# Ubuntu
sudo lsinitramfs /boot/initrd.img-$(uname -r) | grep resume
# Look for: usr/lib/systemd/system/systemd-hibernate-resume.service

If not present, add it permanently and rebuild:

echo 'add_dracutmodules+=" resume "' | sudo tee /etc/dracut.conf.d/resume.conf
sudo dracut --force

This config persists across kernel updates.

Verify it's now included:

# Fedora
sudo lsinitrd /boot/initramfs-$(uname -r).img | grep resume

# Ubuntu
sudo lsinitramfs /boot/initrd.img-$(uname -r) | grep resume
# Should now show systemd-hibernate-resume.service

Step 5: Create systemd sleep config drop-in

sudo mkdir -p /etc/systemd/sleep.conf.d

Create /etc/systemd/sleep.conf.d/hibernate.conf:

[Sleep]
AllowSuspend=yes
AllowHibernation=yes
AllowSuspendThenHibernate=yes
HibernateDelaySec=60min
HibernateMode=platform shutdown
  • HibernateDelaySec=60min — after 60 minutes of suspend, hibernate automatically. Adjust to taste (e.g. 30min).
  • HibernateMode=platform shutdown — uses ACPI platform method first, then powers off.

Step 6: Create systemd logind config drop-in

sudo mkdir -p /etc/systemd/logind.conf.d

Create /etc/systemd/logind.conf.d/lid.conf:

[Login]
HandleLidSwitch=suspend-then-hibernate
HandleLidSwitchExternalPower=suspend-then-hibernate
HandleLidSwitchDocked=ignore

Note: HandleLidSwitchExternalPower=suspend-then-hibernate means the same behaviour applies on AC power. Change to ignore if you use your laptop docked with the lid closed.

Step 7: Create a polkit rule — Ubuntu only, skip on Fedora

On Ubuntu 26.04+, polkit does not correctly map processes running inside user app scopes (e.g. terminals launched from a Wayland compositor) to the active session — so implicit active: yes never triggers and systemctl hibernate returns "Access denied".

Create /etc/polkit-1/rules.d/85-hibernate.rules:

polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.login1.hibernate" ||
        action.id == "org.freedesktop.login1.hibernate-multiple-sessions" ||
        action.id == "org.freedesktop.login1.suspend-then-hibernate") {
        if (subject.isInGroup("sudo")) {
            return polkit.Result.YES;
        }
    }
});

This allows any member of the sudo group to hibernate without a password prompt.

Fedora correctly maps active session processes — skip this step.

Step 8: Set kernel resume parameters and reboot

Hibernate needs the kernel to know where to find the saved RAM image on next boot. You need two values: the UUID of the device containing the swap file, and the physical offset of the swap file within that device.

Get the device UUID:

# Find which device your swap file is on
df /swapfile
# e.g. /dev/nvme0n1p2

# Get its UUID
lsblk -o NAME,UUID /dev/nvme0n1p2   # replace with your device

Get the physical resume offset — method depends on filesystem:

# Btrfs — use btrfs inspect-internal (filefrag returns logical offset on Btrfs, which is wrong)
sudo btrfs inspect-internal map-swapfile -r /swapfile
# Example output: 12592384

# ext4 / XFS — use filefrag, take the physical_offset of the first extent
sudo filefrag -v /swapfile | head -6
# Look for the physical_offset in the first data row (ext 0), e.g.:
#    0:        0..       0:   77576192..  77576192:      1:
# The value is 77576192

Do not use filefrag on Btrfs — it returns the logical offset, which differs from the physical offset the kernel needs and will silently break hibernate.

Set the kernel parameters:

Fedora — use grubby:

sudo grubby --update-kernel=ALL \
  --args="resume=UUID=<your-uuid> resume_offset=<your-offset>"

# Verify
sudo grubby --info=DEFAULT | grep args

Ubuntu — edit /etc/default/grub:

# Add resume=UUID=<your-uuid> resume_offset=<your-offset> to GRUB_CMDLINE_LINUX_DEFAULT
sudo nano /etc/default/grub
# e.g.: GRUB_CMDLINE_LINUX_DEFAULT="quiet splash resume=UUID=a1e3233e-... resume_offset=77576192"

sudo update-grub

⚠️ Stop — Reboot Required

The logind config, initrd changes, and kernel parameters all take effect after a reboot. Save all open work before rebooting.

sudo reboot

Verification

Test that hibernate works at all

Run without sudo — polkit handles privilege elevation:

systemctl hibernate

The system should power off. On next boot, your full session should restore. If it reboots cold (no session restore), check the kernel resume parameters match the output of the offset command above.

Test suspend-then-hibernate

Temporarily lower the delay to 2 minutes for testing:

# Edit /etc/systemd/sleep.conf.d/hibernate.conf, set:
# HibernateDelaySec=2min

sudo systemctl daemon-reload

# Trigger manually
systemctl suspend-then-hibernate
# Wait 2+ minutes, then open the lid — should restore from hibernate

Restore to 60min when confirmed working.

Check applied configuration

systemd-analyze cat-config systemd/sleep.conf
systemd-analyze cat-config systemd/logind.conf

Check kernel resume parameters are active

cat /proc/cmdline | grep resume

Check /sys/power/resume is set (not 0:0)

cat /sys/power/resume
# Should show a non-zero device number like 259:2, not 0:0
# If it shows 0:0, the dracut resume module isn't in the initrd — re-check Step 4

Check current battery level

cat /sys/class/power_supply/BAT0/capacity

Troubleshooting

Error Cause Fix
Invalid resume config: resume= is not populated yet resume_offset= is /sys/power/resume is 0:0 — dracut resume module missing from initrd Add add_dracutmodules+=" resume " to /etc/dracut.conf.d/resume.conf and run sudo dracut --force, then reboot
Access denied on Fedora SELinux denying systemd-logind access to the swap file Run sudo ausearch -m avc -ts recent; apply semanage fcontext and restorecon from Step 2
Access denied on Ubuntu polkit not mapping app-scoped processes to the active session Create the polkit rule from Step 7
Session restores but then freezes Btrfs resume offset is wrong Re-run sudo btrfs inspect-internal map-swapfile -r /swapfile and update resume_offset=, then reboot

Rollback: Undo Everything

# Disable swap file
sudo swapoff /swapfile

# Remove from fstab (delete the /swapfile line)
sudo nano /etc/fstab

# Remove systemd drop-ins
sudo rm /etc/systemd/sleep.conf.d/hibernate.conf
sudo rm /etc/systemd/logind.conf.d/lid.conf
sudo systemctl restart systemd-logind

# Remove dracut config and rebuild initrd
sudo rm /etc/dracut.conf.d/resume.conf
sudo dracut --force

# Remove kernel parameters:

# Fedora
sudo grubby --update-kernel=ALL --remove-args="resume resume_offset"

# Ubuntu
sudo nano /etc/default/grub   # remove resume= and resume_offset= from GRUB_CMDLINE_LINUX_DEFAULT
sudo update-grub

# Ubuntu only — remove polkit rule
sudo rm /etc/polkit-1/rules.d/85-hibernate.rules

# Fedora only — remove SELinux fcontext rule
sudo semanage fcontext -d "/swapfile"

# Reboot for kernel parameter changes to take effect
sudo reboot

# After reboot, delete the swap file
sudo rm /swapfile

Notes

  • Tested on Fedora 44 (systemd 259, kernel 6.19.9) and Ubuntu 26.04 (systemd 259, kernel 7.0.0).
  • Ubuntu 26.04+ uses dracut to build its initrd under the hood, despite update-initramfs still being the frontend command. The dracut module approach works on both distros.
  • On Ubuntu, lsinitramfs is the frontend for listing initrd contents; on Fedora, use lsinitrd.
  • btrfs inspect-internal map-swapfile -r requires btrfs-progs (installed by default on Fedora; sudo apt install btrfs-progs on Debian/Ubuntu).
  • Do not use filefrag to get the swap offset on Btrfs — it returns the logical offset, which differs from the physical offset the kernel needs. Always use btrfs inspect-internal map-swapfile -r.
  • If the swap file's physical offset changes (e.g. after a Btrfs balance or defragmentation), re-run the offset step and reboot.
  • zram swap remains active alongside /swapfile and continues to be used for normal paging (it's faster). systemd correctly selects the NVMe-backed swap file for hibernate.
  • HandleLidSwitchExternalPower=suspend-then-hibernate ensures the same behaviour on AC — the laptop will hibernate after 60 minutes even when plugged in. Change to suspend if you prefer it to stay suspended indefinitely on AC.
  • Run systemctl hibernate without sudo. Using sudo changes the session/process context and can cause "Access denied" even with polkit correctly configured.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment