Bali, Indonesia
Insights

Prune Old Proxmox Snapshots Before They Fill the LVM-Thin Pool

October 31, 2026
0:00
A snapshot taken before an upgrade on Proxmox takes a second and seems to cost nothing. The upgrade goes fine, everyone moves on, and the snapshot stays. Weeks later the LVM-thin pool is nearly full, and nobody can point to which VM grew. Often the answer is the snapshots nobody remembers, and the reason they grow is in how thin provisioning tracks blocks. This matters at the support desk because the symptom rarely looks like a storage problem. A customer reports a VM that froze, or a guest filesystem that suddenly went read-only. The host has plenty of free space according to df, because df doesn't see inside the thin pool at all.
LVM-thin is built on the kernel's device-mapper thin provisioning target, usually called dm-thin. A thin pool is two hidden logical volumes working together. The data volume holds the actual blocks. The metadata volume holds a mapping for every thin volume that says which virtual block of that disk lives at which physical block in the data volume. It also keeps a reference count for each physical block. A thin volume, like a VM disk, starts out with almost nothing allocated. Its 100 GB size is a promise, not a reservation. Physical blocks get allocated from the pool only when the guest writes to a region for the first time. Allocation happens in chunks, and the chunk size is set when the pool is created. When you take a snapshot, dm-thin creates a new thin device that shares the origin's mapping. No data is copied. Every block the disk currently uses now has a reference count of two, one from the origin and one from the snapshot. That is why snapshot creation is nearly instant and appears to use zero space.
The cost shows up on the next write. When the running VM writes to a block that is shared with the snapshot, dm-thin cannot overwrite it in place, because the snapshot still needs the old contents. The kernel follows a simple sequence:
  1. It allocates a fresh chunk from the pool's data space.
  2. If the write covers only part of the chunk, it copies the old chunk contents into the new one first. This is the copy-on-write step.
  3. It writes the new data into the fresh chunk.
  4. It updates the origin's mapping to point at the new chunk, and drops the old chunk's reference count to one.
The old chunk now belongs only to the snapshot. Every block the VM rewrites after the snapshot adds one more chunk that only the snapshot holds. A database server, a mail server, or anything with busy logs rewrites a lot of blocks, so its snapshot keeps growing for as long as it exists. Chunk size makes this worse with small random writes. A 4 KB write into a shared chunk still allocates a whole chunk. Multiple snapshots of the same disk stack this up. Each one pins the version of every block that existed when it was taken. Several snapshots spread over months can, in the worst case, hold several divergent copies of the hot parts of the disk. A snapshot taken with RAM included adds a separate state volume roughly the size of the VM's memory. Deleting files inside the guest doesn't help either. Even with discard enabled, a TRIM from the guest only releases a chunk from the origin's mapping. If the snapshot still references that chunk, its count stays above zero and the space stays used. Metadata grows too. Every new mapping and every reference count change is an entry in the metadata volume. Lots of small scattered writes across many snapshots consume metadata faster than the data numbers suggest. The metadata volume is small compared to the data volume, so it can run out first.
Running out of data space and running out of metadata space behave differently, and neither is graceful. When data space is exhausted, dm-thin by default queues incoming writes instead of failing them, giving an admin time to extend the pool. This is the freeze. Guest I/O blocks and processes waiting on disk hang. After a timeout, controlled by the no_space_timeout parameter of the dm_thin_pool module, the queued writes fail with errors. Guests then see I/O errors, and filesystems like ext4 commonly remount read-only to protect themselves. A pool can be set to error immediately instead with LVM's --errorwhenfull option, which trades the freeze for faster failure. When metadata space is exhausted, the pool can no longer record new mappings, so it typically drops into read-only or failed mode. Recovery may involve taking the pool offline and running lvconvert --repair, which is a much worse afternoon than deleting a few snapshots. Most of the effort here is about never getting near that point. If you are chasing a slow VM rather than a full pool, the iowait side of that story is covered in High iowait in a Proxmox VM: Is It the Guest Disk or the Host Storage?.
The pool's real state is visible with lvs. On a default install the volume group is usually pve and the pool is data, but check your own storage configuration. This command is read-only:
BASH
# Pool and volume usage in volume group 'pve' (read-only)
lvs -o lv_name,lv_size,pool_lv,origin,data_percent,metadata_percent pve

# Include the hidden data and metadata sub-volumes
lvs -a pve
Illustrative output format (values are made up and round):
TEXT
  LV                               LSize   Pool Origin        Data%  Meta%
  data                             800.00g                    90.00  40.00
  root                             96.00g
  snap_vm-100-disk-0_pre-upgrade   100.00g data vm-100-disk-0
  vm-100-disk-0                    100.00g data               60.00
The two numbers that matter are on the data line. Data% is how much of the pool's physical data space is allocated. Meta% is how full the metadata volume is. Watch both. Comfortable data usage with climbing metadata usage is a warning sign, not a pass. The storage summary in the web UI focuses on data usage, so lvs is the easier place to see metadata. Per-volume Data% is easy to misread. It shows how much of that volume's virtual size is mapped, including chunks shared with other volumes. It does not show how much space a snapshot uniquely holds. That is why adding up every volume's percentage can exceed what the pool reports. There is no simple column for a snapshot's unique cost. The practical test is to delete it and watch the pool's Data% drop. The Origin column ties each snapshot volume to its disk, and the volume name usually carries the snapshot name. Snapshot volumes are often inactive and may show no Data% until activated. Their age is easier to read from Proxmox itself.
Proxmox tracks snapshots per guest, and deleting them through Proxmox keeps the guest configuration and the LVM volumes consistent. Avoid lvremove on snapshot volumes directly, since it leaves stale entries in the guest config.
BASH
# List snapshots for every VM on this node (read-only)
for id in (qm list | awk 'NR>1 {print1}'); do echo "== VM id"; qm listsnapshot "id"; done

# Containers use pct instead of qm
pct listsnapshot 200

# Delete one snapshot after confirming nobody needs it. This cannot be undone.
qm delsnapshot 100 pre-upgrade
Before deleting, confirm with whoever took the snapshot that the change it protected has been verified and that a real backup exists. A snapshot lives in the same pool as the disk, so it is a rollback point, not a backup. Deleting a snapshot that sits in the middle of a chain is fine on LVM-thin. The other snapshots keep their own mappings. Expect the delete to take a moment on busy pools, since the kernel walks and frees the snapshot's mappings. A routine that works for most teams has three parts. First, name snapshots so their purpose and date are obvious, such as pre-php-upgrade-2026-10, so a review doesn't turn into guesswork. Second, review the list on a schedule, weekly on busy nodes, and treat anything older than the change it protected as a deletion candidate. Third, alert on Data% and Meta% for the pool itself. A cron job running lvs --noheadings -o data_percent,metadata_percent pve/data and comparing against a threshold is enough. Pick thresholds that leave room to react, typically well before 90 percent. LVM can also grow a pool automatically through thin_pool_autoextend_threshold and thin_pool_autoextend_percent in /etc/lvm/lvm.conf. It only works if the volume group has free extents, and default Proxmox installs often hand nearly everything to the pool. Check vgs before counting on it. Metadata can be grown manually with lvextend --poolmetadatasize when the volume group has room, which buys time but doesn't remove the reason it filled. The short version: a thin snapshot costs nothing on day one and a little more every day the VM keeps writing. Check Data% and Meta% on the pool, not just df on the host. Delete snapshots through qm or pct once the change they protected is confirmed. Give every snapshot a name that tells the next person when it's safe to remove.