Introduction
Proxmox Backup Server's deduplication makes it easy to forget that backups take up space at all. Every nightly job feels almost free — a few hundred megabytes of changed chunks instead of a full VM image — right up until you check the datastore and find it's 90% full anyway, because you never actually deleted anything. You've been keeping every single restore point since the day you set PBS up.
That's what pruning and garbage collection are for. Pruning decides which backup snapshots you no longer need, based on a retention policy you define. Garbage collection is the separate process that actually reclaims the disk space those pruned snapshots were using. Skip either one, or misunderstand how they interact, and you'll either run out of space unexpectedly or lose backups you meant to keep.
This guide walks through configuring both, from the GUI and the command line, on Proxmox Backup Server 4.x. The retention logic and CLI syntax have been stable since PBS 2.x, so everything here applies just as well if you're still on an older release.
What You Will Learn
- The difference between pruning and garbage collection, and why running one without the other does nothing
- How each of the six retention options —
keep-last,keep-hourly,keep-daily,keep-weekly,keep-monthly,keep-yearly— actually decides what survives a prune - How to create and schedule a prune job from the PBS web interface and from
proxmox-backup-manager - What garbage collection is doing internally during its mark and sweep phases, and why it needs a 24-hour-5-minute grace period
- How to schedule garbage collection, and why running it daily is usually a waste
- Realistic retention policy examples for a homelab, a small business, and a compliance-sensitive environment
- Why your datastore might not be shrinking even though pruning is "working"
Pruning vs. Garbage Collection: Two Different Jobs
It's worth being precise about this before touching any configuration, because the two terms get used interchangeably and that's exactly what causes confusion later.
Pruning operates on backup snapshots — the individual restore points you see listed under a backup group in the PBS interface. When a prune job runs, it looks at the snapshots for a group, applies your retention rules, and removes the metadata for any snapshot that doesn't make the cut. After a prune, a snapshot you no longer need simply disappears from the list.
Here's the part that trips people up: pruning a snapshot does not delete the data. A PBS datastore stores backup data as content-defined chunks, and the same chunk is frequently referenced by dozens of different snapshots across different backup groups — that's the whole point of deduplication. Deleting a snapshot's metadata just removes one reference to those chunks. The chunks themselves stay on disk until something confirms that no other snapshot still needs them.
That something is garbage collection. GC scans every chunk in the datastore, checks whether any remaining snapshot still references it, and deletes the ones that don't. This is the step that actually shrinks your used disk space.
So the sequence that frees space is always: prune removes snapshot references → garbage collection later removes the now-unreferenced chunks. If you only schedule pruning and never run GC, your snapshot list stays tidy but your disk usage never goes down. If you only run GC without ever pruning, there's nothing unreferenced for it to find, and it does almost nothing.
Understanding the Retention Options
PBS prune jobs are governed by six retention counters. All of them are optional — you can set any combination, and omitting one means "don't keep anything based on that rule."
| Option | What it keeps |
|---|---|
keep-last N | The N most recent snapshots, full stop, regardless of when they were taken |
keep-hourly N | The most recent snapshot in each of the last N calendar hours |
keep-daily N | The most recent snapshot in each of the last N calendar days |
keep-weekly N | The most recent snapshot in each of the last N ISO weeks (Monday–Sunday) |
keep-monthly N | The most recent snapshot in each of the last N calendar months |
keep-yearly N | The most recent snapshot in each of the last N calendar years |
The rules are processed in that order, and a snapshot only needs to satisfy one of them to survive — the options are additive, not stacked as separate deductions. Practically, that means a policy like:
keep-last=3
keep-daily=7
keep-weekly=4
keep-monthly=6
...keeps your 3 most recent backups no matter what, then the most recent backup from each of the last 7 days, then the most recent from each of the last 4 weeks, then the most recent from each of the last 6 months. A daily backup job under this policy ends up with roughly 3 + 7 + 4 + 6 = up to 20 snapshots retained (fewer in practice, since the same snapshot often satisfies more than one rule — the most recent daily backup is usually also that week's "weekly" keeper).
One detail that matters in practice: these rules only decide what's kept within their own time window. keep-monthly=6 doesn't reach back and rescue a snapshot from 8 months ago — it only looks at the last 6 calendar months. If you want backups preserved indefinitely (for compliance or an annual audit snapshot), keep-yearly with a generous N, or manually protecting a specific snapshot, is the right tool — not stretching one of the other counters unreasonably high.
Manually Protecting a Snapshot
Sometimes you need one specific backup to survive every prune run regardless of retention math — before a risky upgrade, for example. PBS supports this natively:
proxmox-backup-manager snapshot protected \
--group vm/100 --backup-time 2026-07-15T02:00:00Z --protected true
You can also toggle this from the GUI by opening the snapshot's row in the datastore's Content tab and clicking the padlock icon. A protected snapshot is skipped by prune entirely, and it also blocks garbage collection from touching any chunk it references, until you unprotect it.
Creating a Prune Job from the Web Interface
Prune jobs in PBS are configured per-datastore, and can optionally be scoped to a specific namespace within that datastore.
- Log in to the PBS web interface and select your datastore in the left-hand tree.
- Open the Prune & GC tab.
- Click Add under the Prune Jobs panel.
- Set the Namespace (leave as root unless you're organizing backups into namespaces) and, optionally, a Max Depth if you want the job to recurse into sub-namespaces.
- Fill in the retention fields —
Keep Last,Keep Hourly,Keep Daily,Keep Weekly,Keep Monthly,Keep Yearly— leaving any you don't want to use blank. - Set a Schedule using PBS's calendar event syntax, for example
dailyor03:30for 3:30 AM every day. - Click Create.
You can also run a one-off prune manually against a single backup group from the datastore's Content tab by selecting the group and clicking Prune, which opens the same retention fields for a single run instead of a recurring schedule.
Creating a Prune Job from the Command Line
Everything the GUI does maps directly onto proxmox-backup-manager prune-job, which is the more convenient route if you're scripting datastore setup or managing PBS through Ansible/Terraform.
# Create a scheduled prune job
proxmox-backup-manager prune-job create daily-prune \
--store mydatastore \
--keep-last 3 \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 2 \
--schedule "03:30"
# List existing prune jobs
proxmox-backup-manager prune-job list
# Update an existing job's retention
proxmox-backup-manager prune-job update daily-prune --keep-monthly 12
# Disable a job without deleting it
proxmox-backup-manager prune-job update daily-prune --disable true
# Delete a job entirely
proxmox-backup-manager prune-job remove daily-prune
Prune job definitions live in /etc/proxmox-backup/prune.cfg if you ever need to inspect or version-control them directly, though editing that file by hand isn't recommended over the CLI or GUI.
Before trusting a new retention policy against real backups, run it in dry-run mode against a group so you can see exactly what would be removed without committing to it:
proxmox-backup-manager prune \
--group vm/100 \
--keep-last 3 --keep-daily 7 --keep-weekly 4 \
--dry-run true
This prints the list of snapshots that would be kept and removed, which is the fastest way to catch a retention policy that's more (or less) aggressive than you intended before it runs unattended at 3:30 AM.
How Garbage Collection Actually Works
Garbage collection runs in two phases, and understanding both explains most of the "why hasn't my space been freed yet" questions people run into.
Phase 1: Mark
PBS scans every backup index file in the datastore — meaning every snapshot that currently exists, after pruning has already removed the ones you don't want — and for every chunk referenced by those indexes, it touches the chunk file to update its access time (atime). This is a "mark" pass: anything still in use gets its atime bumped to "now."
Phase 2: Sweep
PBS then walks every chunk actually stored on disk and compares its atime against a cutoff. Any chunk whose atime is older than the cutoff clearly wasn't touched in phase 1, meaning no current snapshot references it, so it gets deleted.
Why There's a Grace Period
The cutoff isn't "right now" — it's the start of the GC run minus 24 hours and 5 minutes (or the start time of the oldest currently-running backup upload, if one is in progress, whichever is more conservative). That gap exists because Linux filesystems are commonly mounted with relatime, which only updates a file's atime once per day under normal read access, not on every single read. Without the grace period, a chunk that's genuinely in use but happened to have its atime updated more than a day ago by a filesystem that batches atime updates could be mistaken for garbage and deleted mid-backup. The 24h5m window guarantees phase 1's atime bump is reliably newer than the cutoff for anything actually referenced, even under relatime.
The practical consequence: if you prune a bunch of old snapshots and immediately kick off garbage collection, you might not see much (or any) space reclaimed. GC will only sweep chunks whose atime predates that 24h5m window — chunks freed by a prune you ran ten minutes ago haven't had time to fall outside it yet. Run GC again the next day and you'll typically see the reclaim show up.
Scheduling Garbage Collection
Unlike prune jobs, which are per-group and can be scheduled multiple times with different scopes, a datastore has exactly one GC schedule, set at the datastore level.
Via the GUI: Datastore → Prune & GC tab → set the Garbage Collection schedule field.
Via the CLI:
# Set a weekly GC schedule
proxmox-backup-manager datastore update mydatastore --gc-schedule "sun 02:00"
# Trigger GC immediately, outside its schedule
proxmox-backup-manager garbage-collection start mydatastore
# Check the status/result of the last GC run
proxmox-backup-manager garbage-collection status mydatastore
# Remove the schedule (disable automatic GC)
proxmox-backup-manager datastore update mydatastore --delete gc-schedule
Garbage collection is I/O- and CPU-intensive — it has to read the atime of literally every chunk in the datastore, which on a large datastore with millions of small chunks can take hours and compete with active backup jobs for disk bandwidth. Because of the 24h5m grace period, running it more than once a day buys you essentially nothing: nothing new becomes eligible for deletion faster than that. A weekly schedule (Sunday at 2 AM, for example, timed to land after your last daily backup job of the week) reclaims the same total space as a daily schedule with a fraction of the overhead.
Where Verification Fits In
Pruning and GC manage space; they say nothing about whether the chunks that remain are actually intact. That's what a separate verify job is for — it reads back chunks and checks them against their stored checksums, catching silent disk corruption (bit rot) long before you'd discover it during an actual restore.
# Run a manual verification
proxmox-backup-manager verify mydatastore --read-threads 1 --verify-threads 4
# Skip snapshots that already passed verification recently
proxmox-backup-manager verify mydatastore --ignore-verified true --outdated-after 30
It's worth calling out because of one interaction: a verify job that hasn't run recently gives you false confidence that "the backups are fine" when all pruning and GC have actually confirmed is that the metadata is consistent. Schedule verification independently (monthly re-verification of everything is the commonly recommended baseline) rather than assuming a clean prune/GC history means your data is readable.
Retention Policy Examples
There's no universally correct policy — it depends on how much history you actually need and how much datastore space you're willing to dedicate to it. A few starting points:
Homelab, single node, nightly backups
keep-last=2
keep-daily=7
keep-weekly=4
keep-monthly=3
Enough to recover from "I broke something this week" without needing more than about three months of history, and cheap enough that deduplication keeps it small even on modest storage.
Small business, hourly + nightly jobs on production VMs
keep-hourly=12
keep-last=4
keep-daily=14
keep-weekly=8
keep-monthly=12
Hourly retention covers same-day "restore to an hour ago" scenarios (ransomware, bad deploys), while the daily/weekly/monthly tiers give a full year of monthly checkpoints for slower-to-notice problems.
Compliance-sensitive environment
keep-last=7
keep-daily=30
keep-weekly=12
keep-monthly=24
keep-yearly=7
Combine a policy like this with protected snapshots for any backup tied to a specific audit period, since keep-yearly only guarantees the most recent snapshot per year survives — not a specific one you're required to retain regardless of retention math.
Troubleshooting
Datastore usage isn't going down after pruning
Check whether a GC schedule is actually configured (proxmox-backup-manager datastore list --output-format json and look for gc-schedule), not just a prune schedule — this is the single most common cause. If GC is scheduled, confirm it has actually run since the prune, and remember the 24h5m grace period means chunks freed very recently won't be swept until the next GC run after that window passes.
GC status shows "unable to acquire lock"
Only one garbage collection task can run per datastore at a time, and it also can't run concurrently with certain other datastore-maintenance operations. If a previous GC run is still in progress (check Datastore → Tasks), wait for it to finish rather than force-starting another one.
Prune job runs but nothing gets removed
Double-check the retention numbers aren't more generous than your actual backup history — keep-daily=30 on a datastore that's only three weeks old won't remove anything yet, because every existing snapshot still falls within the window. Use --dry-run true against the actual retention settings to see exactly what the job considers keep-worthy.
A snapshot you needed was already pruned
There's no undelete for a pruned snapshot once GC has swept its chunks — this is why testing new retention policies with --dry-run before applying them matters, and why anything you might need indefinitely should be explicitly protected rather than relying on a keep-* counter to happen to cover it forever.
Best Practices
- Always pair a prune schedule with a GC schedule on the same datastore — a prune job with no corresponding GC just accumulates unreferenced chunks that never get cleaned up.
- Schedule GC weekly, not daily, on anything but a very small datastore — the 24h5m grace period means daily runs mostly re-scan chunks that were already confirmed live the day before.
- Test new or changed retention policies with
proxmox-backup-manager prune --dry-run truebefore letting them run unattended against real backup history. - Use
protectedsnapshots for anything that must survive indefinitely (pre-upgrade checkpoints, compliance snapshots) instead of tuning a keep-* counter high enough to "probably" cover it. - Schedule verification jobs independently of prune/GC — a healthy prune and GC history tells you nothing about whether the remaining chunks are actually readable.
- If you manage multiple datastores or namespaces, give prune jobs descriptive names (
vm-nightly-prune, notprune-job-1) — it matters once you're troubleshooting which job did what from the task log.
Frequently Asked Questions
Do I need to run garbage collection manually after every prune?
No — schedule both jobs and let them run independently. Because of the 24h5m grace period, running GC immediately after a prune usually won't reclaim much space anyway; a weekly GC schedule that runs after your prune jobs have had time to accumulate is more efficient.
What happens if I never configure a prune job at all?
Every backup ever taken stays forever, and the datastore only grows. Deduplication slows that growth but doesn't stop it — new data (updated files, new VMs) still needs new chunks regardless of how well old data is deduplicated.
Can I have different retention policies for different VMs?
Yes. Prune jobs can be scoped to a specific namespace, and if you organize backups into per-VM or per-project namespaces you can give each one its own prune job with its own retention settings, rather than applying one policy to the whole datastore.
Does pruning affect backups that Proxmox VE thinks it has?
Yes — PBS is the source of truth for what backups exist. Once a snapshot is pruned, it disappears from the VM's Backup tab in Proxmox VE as well, since that list is populated by querying the PBS datastore directly.
Is it safe to lower keep-* values on an existing prune job?
The change itself is safe — it only takes effect the next time the job runs. But the next run will prune more aggressively based on the new, lower numbers, potentially removing snapshots you're used to having around. Run a dry-run first if you're not sure exactly which snapshots will go.
Why does GC take so long on a large datastore?
Phase 2 has to stat every chunk file on disk to check its atime, and a datastore holding years of deduplicated backups can easily have millions of small chunk files. This is inherently I/O-bound, which is also why running GC on fast storage (or giving it a dedicated window with less backup traffic competing for I/O) meaningfully affects how long it takes.