Skip to content

Instantly share code, notes, and snippets.

@shift
Created March 7, 2026 11:26
Show Gist options
  • Select an option

  • Save shift/6e2c668390ff2cb4319932a7332396ad to your computer and use it in GitHub Desktop.

Select an option

Save shift/6e2c668390ff2cb4319932a7332396ad to your computer and use it in GitHub Desktop.
{ config, lib, pkgs, ... }:
let
cfg = config.services.swap-sentinel;
# Naming it 'swap-sentinel' to ensure uniqueness.
storagePath = "/var/lib/swap-sentinel";
in {
options.services.swap-sentinel = {
enable = lib.mkEnableOption "Dynamic Btrfs swapfile manager";
minFreeMB = lib.mkOption {
type = lib.types.int;
default = 512;
description = "Threshold of free swap in MB before creating a new file.";
};
chunkSizeGB = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Size of each dynamic swap chunk.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.swap-sentinel = {
description = "Dynamic Swap Sentinel for Btrfs";
after = [ "local-fs.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
bash
gawk
procps
util-linux
coreutils
btrfs-progs
];
serviceConfig = {
Type = "simple";
ExecStartPre = "${pkgs.bash}/bin/bash -c 'mkdir -p ${storagePath} && chattr +C ${storagePath} || true'";
ExecStart = let
script = pkgs.writeShellScript "swap-sentinel-run" ''
set -euo pipefail
CHUNK_SIZE_MB=$(( ${toString cfg.chunkSizeGB} * 1024 ))
THRESHOLD=${toString cfg.minFreeMB}
echo "Sentinel started. Monitoring swap on ${storagePath}..."
while true; do
# Get current free swap in MB
FREE_SWAP=$(free -m | awk '/^Swap:/ {print $4}')
if [ "$FREE_SWAP" -lt "$THRESHOLD" ]; then
TIMESTAMP=$(date +%s)
SWAPFILE="${storagePath}/extra-$TIMESTAMP.swap"
echo "Low swap ($FREE_SWAP MB). Creating $SWAPFILE..."
# Btrfs specific swapfile creation
truncate -s 0 "$SWAPFILE"
chattr +C "$SWAPFILE"
fallocate -l "$(( ${toString cfg.chunkSizeGB} ))G" "$SWAPFILE"
chmod 600 "$SWAPFILE"
mkswap "$SWAPFILE"
swapon "$SWAPFILE"
elif [ "$FREE_SWAP" -gt "$(( CHUNK_SIZE_MB + THRESHOLD + 512 ))" ]; then
# Check if we have dynamic files to remove (ignore the 16GB hibernation file)
OLDEST_DYNAMIC=$(ls -t ${storagePath}/extra-*.swap 2>/dev/null | tail -n 1 || true)
if [ -n "$OLDEST_DYNAMIC" ]; then
echo "High swap headroom ($FREE_SWAP MB). Removing $OLDEST_DYNAMIC..."
swapoff "$OLDEST_DYNAMIC"
rm "$OLDEST_DYNAMIC"
fi
fi
sleep 30
done
'';
in "${script}";
Restart = "always";
};
};
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment