How to Set Up Hibernation on Linux (Swap Done Right)

I recently reinstalled my main desktop from scratch: Debian testing via Kali underneath (read first before judging) and XFCE on top. One of the reasons I keep coming back to XFCE is its built-in power management. Hibernation in particular has never once glitched on me, on any hardware I’ve run it on. I don’t shut down my work machine, a tower. I hibernate it, and every session picks up exactly where the last one ended: the same terminal tabs, same GIMP tabs, same music track, same pile of browser tabs I keep telling myself I’ll close and so on.

That habit is what exposed the problem. This box has 16 GB of RAM, and after a few weeks of hibernating instead of rebooting, memory gets tight. Eventually free memory is nearly gone and there’s barely anything left for cache. The honest fix is more RAM. But this is also my gaming rig, it runs two sticks, adding two more would drop the memory frequency, and replacing the pair costs real money. So I make do.

Making do means swap does real work. And my 16 GB swap partition, fresh from the install, was doing two jobs at once: absorbing everyday paging and holding the hibernation image. The fuller it got with paged-out memory, the less room there was for the image. Hibernate would work for a while, then get tight.

btop and glances on a my Linux desktop showing memory usage and 31.9 GB total swap after adding a swapfile for hibernation

The fix turned out to be simple, and it works on virtually any Linux distribution that supports hibernation: run two swap spaces with different jobs. A swapfile handles everyday paging. The swap partition stays mostly empty, reserved for hibernation. No repartitioning, no live USB, no resume_offset math.

This guide walks through the whole setup step by step.

Why hibernation fails when swap does double duty

When you hibernate, the kernel compresses the contents of RAM into an image and writes it to your resume device, which is almost always your swap partition. The kernel’s swap suspend documentation covers the mechanics, but the practical requirement is this: there must be enough free space in that swap area to hold the image at the moment you hibernate. The image is compressed. By default the kernel aims to keep it under about 2/5 of your RAM (cat /sys/power/image_size shows the cap in bytes), so it’s usually far smaller than installed memory. How much space you need in practice depends on how compressible your RAM contents happen to be at that moment.

Here’s the trap. That same swap partition is also where the kernel pages out cold memory during normal use. If you add swap space sized equal to your RAM (the common advice, and what most installers set up), it looks like plenty. It isn’t. Run for a week without rebooting and several gigabytes of idle application memory drift into swap. Now your 16 GB partition might have 9 GB free, and the hibernation image doesn’t fit. Hibernate fails, usually with a vague error, sometimes with none at all.

Two things make this worse on a desktop or laptop you never shut down:

  • Swap usage tends to persist. Pages come back into RAM when something touches them, but the kernel won’t proactively pull cold pages out of swap just because free RAM opens up. Over long uptimes, usage creeps.
  • You can’t fix it with zram or zswap alone. A zram device lives in RAM, so it cannot hold a hibernation image. It vanishes the moment power drops.

So the swap partition has two jobs that compete for the same space. Priorities let you separate them.

The fix: two swap spaces, two jobs

Terminal output of mkswap, adding the swapfile to /etc/fstab with pri=100, and swapon --show listing the swap partition at priority -1 and swapfile at priority 100

Every swap area in Linux has a priority. The kernel fills higher-priority swap first and only spills into lower-priority areas when the first one is full. That single mechanism is all we need.

Create a swapfile and give it a high priority, say 100. Your existing swap partition keeps its default kernel-assigned priority of -1 (or -2 on some kernels). Everyday paging now lands in the swapfile. The partition sits idle, which is exactly what hibernation needs. One caveat: priorities don’t hard-reserve anything. Fill the swapfile completely and the kernel spills into the partition, so size the swapfile to your real paging habits and that stays rare.

Hibernation itself doesn’t care about priorities at all. The image gets written to whatever resume= points at, not to the highest-priority swap. Paging follows priority. Hibernation follows the resume device. Two different mechanisms, and that’s the whole trick.

On my system it ends up like this:

Swap area Size Priority Role
/swapfile 16 GB 100 Everyday paging
/dev/nvme0n1p3 15.9 GB -1 Overflow + hibernation target

Total swap roughly doubles to ~32 GB, and the resume device stays free for the image. Not bad for five minutes of work.

Step 1: Check your current setup

First, see what swap you have and how it’s prioritized:

swapon --show
NAME           TYPE      SIZE   USED PRIO
/dev/nvme0n1p3 partition 15.9G  6.3G   -1

That USED column is the problem in one line. 6.3 GB already gone to ordinary paging, under 9 GB left for a hibernation image, and the machine had only been up for a few days. If you want to see exactly which processes own that swapped memory, smem breaks it down per process.

Next, confirm your resume device. This is the swap area the kernel writes the hibernation image to:

grep -o 'resume=[^ ]*' /proc/cmdline
resume=UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890

Make sure that UUID is your swap partition and not something else. lsblk confirms it: lsblk -no UUID /dev/nvme0n1p3 (swap in your device name) should print the same value. Match? Good. We won’t be touching it. If it returns nothing, jump ahead to Step 4 after creating the swapfile, because hibernation was never fully configured on your system in the first place.

Finally, check free disk space on the filesystem where the swapfile will live:

df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  916G  474G  396G  55% /

396 GB free. A 16 GB swapfile is a rounding error.

Step 2: Create the swapfile

On ext4 or XFS, this takes four commands:

sudo fallocate -l 16G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon -p 100 /swapfile

The -p 100 is the important part. That’s the priority that makes the kernel fill this file before touching the partition.

Size it to your paging habits. I matched my RAM at 16 GB, which is generous. If your current swap usage never crosses a few GB, 8 GB is fine. You can always delete the file and make a bigger one later; that’s the advantage of a file over a partition.

A few filesystem caveats:

  • Btrfs: swapfiles need the No_COW attribute and no compression. On btrfs-progs 6.1 or newer, let the tooling handle it: sudo btrfs filesystem mkswapfile --size 16g /swapfile, then swapon as above. Hibernating directly to a Btrfs swapfile is its own can of worms. This setup never does that; the partition stays the resume device.
  • Older kernels, XFS in particular: XFS only supports preallocated swapfiles since kernel 4.18. On anything older (RHEL 7 and friends), fallocate produces a file that swapon rejects with a confusing Invalid argument. Write the file for real with dd instead: sudo dd if=/dev/zero of=/swapfile bs=1M count=16384 status=progress (count is in MiB, so match it to your chosen size). Same fallback anywhere swapon complains about holes.
  • ZFS: don’t put a swapfile on ZFS. Use a zvol or keep swap on a separate filesystem.

Verify both swap areas are live:

swapon --show
NAME           TYPE      SIZE   USED PRIO
/dev/nvme0n1p3 partition 15.9G  6.3G   -1
/swapfile      file        16G     0B  100

One security note before moving on. Everything paged to disk, and the hibernation image itself, is raw memory contents: credentials, session tokens, decrypted files. On a laptop that can be lost or stolen, unencrypted swap is a real exposure, and the kernel’s own hibernation documentation recommends encrypted swap for this reason. If your install uses full-disk encryption, both swap areas are already covered. If not, factor that into whether hibernation is right for the machine.

Step 3: Make it permanent

Add the swapfile to /etc/fstab so it survives reboots, with the priority baked in:

echo '/swapfile none swap sw,pri=100 0 0' | sudo tee -a /etc/fstab

Run that once, not twice. If /swapfile already has a line in /etc/fstab, edit it instead of appending a duplicate. Leave the existing partition line alone. Its default priority is already lower than 100, which is all that matters. You don’t need to set an explicit priority on it.

Step 4: Verify (or set) the resume device

The beauty of this setup is that the resume device doesn’t change. The swapfile never holds a hibernation image, so it needs no resume_offset, no initramfs rebuild, nothing. If Step 1 showed resume= already pointing at your partition’s UUID, you’re done with this step.

If resume= was missing, set it now. Grab the partition’s UUID:

# Change to your device name, not mine!
lsblk -no UUID /dev/nvme0n1p3

Quick aside on why UUID and not the device path: names like /dev/nvme0n1 are assigned by probe order and are not guaranteed stable across boots, especially with multiple NVMe drives. The drive that’s nvme0n1 today can come up as nvme1n1 after a reboot. A UUID identifies the same partition no matter what the kernel names it.

Then add resume=UUID=your-uuid-here to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub. The resume= parameter is documented in the kernel’s boot parameters reference, and the GRUB manual covers the config file itself if you’re new to editing it. Regenerate GRUB’s config and the initramfs. Exact steps depend on your distro’s initramfs tooling; these cover the common setups:

# Debian / Ubuntu / Kali
echo "RESUME=UUID=your-uuid-here" | sudo tee /etc/initramfs-tools/conf.d/resume
sudo update-initramfs -u
sudo update-grub

# Fedora / RHEL
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo dracut -f

# Arch
sudo grub-mkconfig -o /boot/grub/grub.cfg
sudo mkinitcpio -P

On Arch, also make sure the resume hook is present in /etc/mkinitcpio.conf before regenerating.

Step 5: Test it

sudo systemctl hibernate

The machine should write the image, power off completely, and restore your full session on the next boot. If it fails, two common culprits:

  • “Sleep verb ‘hibernate’ not supported” has a few possible causes: kernel lockdown (common with Secure Boot on), no usable swap, hibernation compiled out of the kernel, or an AllowHibernation=no override somewhere. Check lockdown first: cat /sys/kernel/security/lockdown. Disabling Secure Boot in firmware is the blunt fix for that one. Signed-kernel setups vary by distro.
  • Silent failure or instant wake: journalctl from the previous boot (journalctl -b -1) usually shows why. Behavior around hibernation is also configurable in systemd-sleep.conf if your distro ships overrides. If the journal shows Error -12, see the last section of this article.

One last thing: draining the old swap

swapoff and swapon on the swap partition draining 6.3 GB of old swap, with swapon --show confirming the partition at 0B used and the swapfile taking over

Pages that were already sitting in the partition before you added the swapfile stay there. The kernel won’t migrate them. They’ll drain naturally over days as those pages get touched, or you can force it:

#Your partition's device name goes here, not mine. :)
sudo swapoff /dev/nvme0n1p3 && sudo swapon /dev/nvme0n1p3

Only do this once the swapfile is active and you have enough free RAM to absorb the pages being pulled back in. swapoff on a system with no memory headroom will grind the machine to a crawl. Check that free -h shows comfortable available memory first.

When hibernation fails with Error -12

Full disclosure: while testing this exact setup for this article, hibernation failed on me. Swap was fine. The kernel indicated what was wrong:

journalctl -b -1 | grep -i hibernation
PM: hibernation: Normal pages needed: 2233070 + 1024, available pages: 1941737
PM: hibernation: Error -12 creating image

-12 is ENOMEM. This is because the kernel builds the hibernation image in RAM first, then writes it to swap. It needed about 8.5 GB of free memory to construct the snapshot and I had 7.4. My new 31 GB of swap (16 for hibernate) was never the constraint. The kernel never got as far as writing to it.

The fix is the image_size knob from earlier. Lowering it makes the kernel swap more pages out before taking the snapshot, so the snapshot needs less free RAM. Cap it at 2 GB and test:

echo 2147483648 | sudo tee /sys/power/image_size
sudo systemctl hibernate

That worked immediately on my loaded system. The trade-off is a longer entry into hibernation, since more gets swapped out up front. Setting it to 0 shrinks the image as aggressively as possible if 2 GB still fails under your load.

The value resets every boot, so make it stick with a tmpfiles.d entry. This has to be tmpfiles.d, not sysctl.d, because sysctl only writes under /proc/sys and this lives in /sys:

echo 'w /sys/power/image_size - - - - 2147483648' | sudo tee /etc/tmpfiles.d/hibernate.conf

This matters most for unattended hibernation. Walk away from a loaded machine and the idle timer fires exactly when memory pressure is highest. So there are really two common ways swap-adjacent hibernation fails: not enough free space in the resume area for the image (what this article is mostly about), and not enough free RAM to build the image in the first place. The two-swap setup solves the first. image_size solves the second. Previously betterlockscreen broke it also, but that’s only because I installed Kali-i3 on top of XFCE, so I just had to disable it and replace it with the XFCE default locker.

Conclusion

Hibernation on Linux has a reputation for being flaky. In my experience a lot of that flakiness is really just a full swap partition wearing a trench coat. Separate the two jobs and the problem disappears: the swapfile absorbs daily paging at priority 100, the partition stays out of the paging business so there’s room for the image, and resume= never has to change.

Since making this change, hibernate has worked every single time, including after two-week stretches without a reboot and with GIMP, Termius, and an embarrassing number of browser tabs open. Total swap went from 16 GB to about 32 GB as a side effect. If your machine hibernates for a week and then mysteriously stops, check swapon --show before you check anything else.

Tags: , , , ,

Stop losing users to slow load times.

Blazing-fast, custom-configured NVMe Linux hosting.

Don't guess if your server is fast enough. Know it. Get a free performance audit of your current setup.

This blog is hosted by StackLinux, our parent company. 99.99% uptime.

Start Your Free Server Audit

You write the code. We'll manage the server.

Fully managed Linux hosting built for absolute speed.

Ditch the server maintenance headaches. Our experts custom-configure high-performance NVMe Linux servers specifically for your needs. Try us out. Your first month is entirely on us.

This blog is hosted by StackLinux, our parent company. 99.99% uptime.

Claim Your 30 Free Days

30 days of NVMe Linux hosting. $0 down.

Risk-free migration and a money-back guarantee.

We are so confident in our high-performance NVMe infrastructure that we'll audit your current server for free, migrate you without downtime, and give you 30 days to see the speed difference yourself.

This blog is hosted by StackLinux, our parent company. 99.99% uptime.

Deploy Free for 30 Days
Top ↑