Skip to content
Go back

fstrim vs discard: Keeping SSDs Fast

By KingPin 10 min read
fstrim vs discard: Keeping SSDs Fast
Contents

Your VM Disk Only Ever Grows

You delete 200 GB of old backups inside a VM. The guest says the space is free. The host says the disk image is still 200 GB fatter than it should be. Nothing is broken. The storage underneath never got told about the delete.

The short version: enable fstrim.timer, leave ext4 and XFS mounted without discard, and know that Btrfs on kernel 6.2 or newer already does async discard by default. Then check every layer of the stack, because one layer that drops TRIM requests quietly cancels the whole setup.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/linux/fstrim-and-discard

What TRIM Actually Tells the Drive

When you delete a file, the filesystem marks its blocks as free in its own bookkeeping. It does not tell the device. The SSD, the thin-provisioned LV, or the qcow2 image still believes those blocks hold live data.

A discard (TRIM) request is the filesystem saying “these blocks are free, you can forget them.” On a physical SSD the controller learns the blocks are free. On a thin-provisioned backing store (qcow2, LVM thin, a ZFS zvol) the space actually goes back to the host. Write wear is a separate topic, covered in Is Your Linux Server Destroying Its SSD?.

Two ways to send those requests exist. Periodic: run fstrim on a schedule. Continuous: mount with the discard option so every delete sends one immediately.

The Measured Demo

We wanted numbers, so we built a kit. The script makes a 2 GiB sparse image, attaches it as a loop device, formats it ext4 with -E nodiscard, and mounts it. A sparse file shrinks when the filesystem on it issues discards. That is the same mechanism a thin-provisioned VM disk uses. A physical SSD does not shrink visibly.

Run it from the kit directory:

Terminal window
docker run --rm --privileged -v "$PWD":/work debian:trixie /work/demo.sh

--privileged is there because the script needs a loop device, and an unprivileged container cannot create one. The script cleans up after itself.

This is our run: 2 GiB sparse image, loop device, ext4, kernel 6.8, 2026-11-12. Part 1 mounts without discard and runs fstrim by hand:

--- Part 1: periodic TRIM (mount without discard, run fstrim) ---
after mkfs: 66M on disk
after 1 GiB: 1.1G on disk
after rm: 1.1G on disk
/mnt/t: 1.9 GiB (2039824384 bytes) trimmed
after fstrim: 66M on disk
/mnt/t: 0 B (0 bytes) trimmed
after remount:
/mnt/t: 1.9 GiB (2039824384 bytes) trimmed

Part 2 mounts with -o discard:

--- Part 2: continuous discard (mount -o discard) ---
after 1 GiB: 1.1G on disk
after rm: 66M on disk

Three things to read from that output.

  1. Deleting the file freed it in the filesystem, but the image stayed at 1.1G until fstrim ran.
  2. fstrim reported 1.9 GiB trimmed, not the 1 GiB we wrote. It reports all the free space it discarded, so the number is not “how much garbage there was.”
  3. The second fstrim on the same mount reported 0 B. After unmount and mount, it trimmed 1.9 GiB again.

Periodic TRIM: fstrim and fstrim.timer

Check whether the timer exists and runs on your box. Do not assume. Distros differ, so ask systemd:

Terminal window
systemctl status fstrim.timer

Enable it:

Terminal window
sudo systemctl enable --now fstrim.timer

The unit shipped by util-linux uses these settings:

[Timer]
OnCalendar=weekly
AccuracySec=1h
Persistent=true
RandomizedDelaySec=100min

Persistent=true means a run missed while the machine was off fires at the next boot. The service runs fstrim --listed-in /etc/fstab:/proc/self/mountinfo --verbose --quiet-unsupported. The unit also has ConditionVirtualization=!container, so it does nothing inside a container. Trim from the host or the VM instead.

See when it fires next, and what it did last time:

Terminal window
systemctl list-timers fstrim.timer
journalctl -u fstrim.service

You can also run it by hand. -v prints the amount trimmed, and -n does everything except the actual discard:

Terminal window
sudo fstrim -v /
sudo fstrim --dry-run --all

-a trims every mounted filesystem on devices that support discard. -A trims the ones listed in /etc/fstab. With --all, exit code 0 means everything succeeded, 32 means everything failed, and 64 means some succeeded.

Why the number looks weird

Back to the demo. A first run that reports gigabytes you never deleted is normal, because fstrim counts all free space it discarded. A second run that reports 0 B is also normal: ext4 remembers which block groups it already trimmed during this mount. After a reboot it trims all free space again. So a big number after every boot is not a sign of a problem.

Continuous TRIM: the discard Mount Option

In the demo, -o discard dropped the image to 66M right after rm and sync. No fstrim needed. So why not use it everywhere? Because it costs something on every delete, and the filesystem docs are cautious about it.

An fstab line for it looks like this:

UUID=<your-uuid> /data ext4 defaults,discard 0 2

What the man pages say per filesystem:

So on a modern kernel, Btrfs already does the right thing. Adding a weekly fstrim.timer on top is harmless.

When is continuous discard worth it? When a thin-provisioned backing store needs the space back right now, such as a VM disk on a host that is nearly full. That is a real use. As a default on ext4 or XFS it is not.

Does Your Drive Even Accept TRIM?

Ask the kernel:

Terminal window
lsblk --discard
NAME DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
sda 0 4K 4G 0
nvme0n1 0 512B 2T 0

Non-zero DISC-GRAN and DISC-MAX mean the device accepts discard. Zeros mean it does not, at that layer. The rows above are from a real SATA SSD and a real NVMe drive.

Check each layer: the partition, dm-crypt, LVM, and the filesystem on top. One layer without discard support stops it for everything above.

The Layers That Eat TRIM

This is where most “TRIM isn’t working” tickets end. The filesystem and the drive are fine, and something in between drops the request.

LUKS

Discards are not passed through dm-crypt unless you enable them. One-off, at open time:

Terminal window
sudo cryptsetup open --allow-discards /dev/sdX cryptdata

For LUKS2, store the setting in the header so it survives reboots and needs no crypttab edit:

Terminal window
sudo cryptsetup refresh --allow-discards --persistent cryptdata

Here cryptdata is the name of the mapped device. To remove the flag later, run the same command with --persistent and without --allow-discards.

The other route is the discard option in /etc/crypttab:

cryptdata UUID=<your-uuid> none luks,discard

Confirm it took effect. The crypt line in the device-mapper table should contain allow_discards:

Terminal window
sudo dmsetup table cryptdata

The trade-off is real, so here it is plainly. The cryptsetup man page warns that discards can make filesystem-level operations visible on the physical device. Information such as filesystem type and used space may be extractable from the device if the discarded blocks can be located later. Most home labs accept that. If you need plausible deniability for the disk contents, you do not. Also, discard is not supported for LUKS2 with data integrity protection.

LVM: the issue_discards misconception

Many people set issue_discards = 1 hoping it turns on TRIM. It does not. That setting only controls whether LVM discards space on a PV when an LV frees it, as in lvremove or lvreduce:

/etc/lvm/lvm.conf
devices {
issue_discards = 0 # default
}

Filesystem discards pass through a normal LV regardless of that setting. Be careful turning it on: vgcfgrestore restores only metadata, so data in discarded regions is gone.

Virtual machines

A guest’s fstrim only reaches the host if the virtual disk exposes discard. In QEMU/libvirt, set discard='unmap' on the disk driver element:

<driver name='qemu' type='qcow2' discard='unmap'/>

In Proxmox, tick the “Discard” checkbox on the VM disk. Without it, lsblk --discard inside the guest shows zeros, and the guest’s fstrim fails with “the discard operation is not supported” (the timer hides that error with --quiet-unsupported), so nothing reaches the host.

Swap, briefly

swapon takes --discard[=once|pages], and fstab accepts discard, discard=once, and discard=pages. The man page says this “may improve performance on some Solid State Devices, but often it does not.” Treat it as optional.

ZFS

ZFS handles TRIM itself, with the pool property autotrim (default off) and a manual zpool trim <pool>. The rest lives in ZFS Tuning for SSDs and NVMe.

The SumGuy Take

  1. Run systemctl status fstrim.timer. Enable it if it is not active.
  2. Leave ext4 and XFS mounted without discard.
  3. Leave Btrfs on 6.2+ alone. It is already async.
  4. Use lsblk --discard on every layer, host and guest.
  5. Turn on --allow-discards for LUKS only if you accept the leak described above.
  6. Use continuous discard only when a thin backing store needs space back now.

Your 2 AM self will thank you when the host disk is not mysteriously full.

Common Questions

Is it safe to run fstrim on a mounted filesystem?

Yes. The fstrim tool works on a mounted filesystem, and a mount point is what you pass to it. It discards only free blocks, so your files are untouched. The -n (--dry-run) flag runs everything except the actual discard if you want to rehearse first.

How often should fstrim run?

Weekly is enough for most machines, and weekly is the fstrim.timer default shipped by util-linux, with up to 100 minutes of random delay and catch-up after downtime. Repeat runs are cheap on ext4: an unchanged mount reported 0 B on the second pass in our demo, because already-trimmed block groups get skipped.

Does fstrim work inside a Docker container?

Only in a privileged container that can see the block device, which is how our demo ran fstrim against a loop device. For the host’s real disks, run fstrim on the host. The stock fstrim.timer has ConditionVirtualization=!container, so a systemd instance inside a container skips it.

Does enabling LUKS discard leak data?

Yes, a little metadata. The cryptsetup man page says discards can reveal filesystem type and used space on the physical device if discarded blocks can be located later. File contents stay encrypted. LUKS discard is fine for a home lab and wrong for anyone who needs plausible deniability.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Penpot: Self-Host an Open Source Figma

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts