restic forget and prune: how the keep flags decide
· 10 min read
On this page (10)
Everyone copies --keep-daily 7 --keep-weekly 4 --keep-monthly 6 from a tutorial. Few can say how many snapshots that leaves, which dates survive, or what happens to the policy when the backup job stops for a month. This post explains how the keep flags actually select snapshots, what prune does and why it is the expensive half, and how to design a policy for a small server and check it before it deletes anything.
Who needs this
A developer whose backup repository is slowly filling a bucket. You have been running restic for a year, nothing has ever been forgotten because the forget line was never added, and the bill creeps up. You want a policy that bounds growth without losing the restore points that matter.
Someone who found a bug three weeks after it shipped. A bad migration quietly corrupted a column, nobody noticed, and now you need the data as it was before. Whether a three-week-old snapshot exists depends entirely on the policy you chose months ago.
A team backing up to cloud storage, where prune is slow and costs money. forget --prune after every nightly run rewrites packs in the bucket and moves data both ways. You want the cheap forget often and the expensive prune rarely.
Anyone sharing one repository between several servers or projects. The default grouping rules mean a policy applies per host and path set. Change the path list and your policy quietly splits in two.
Two commands, two jobs
Two commands, two jobs
restic forget decides which snapshots to remove and removes the snapshot records. That is all. It is fast, and it frees no space.
restic prune removes the data that no remaining snapshot references. It is the slow part and the one that frees space.
restic forget --prune runs both in sequence. The distinction matters because the first is a decision about history and the second is housekeeping, and you may want to run them on different schedules.
How the keep flags work
Each --keep-* flag is a separate rule. A snapshot is kept if any rule selects it. Snapshots no rule selects are removed.
| Flag | What it keeps |
|---|---|
--keep-last N | the N most recent snapshots |
--keep-hourly N | for each of the last N hours that have snapshots, the newest one |
--keep-daily N | for each of the last N days that have snapshots, the newest one |
--keep-weekly N | for each of the last N weeks that have snapshots, the newest one |
--keep-monthly N | for each of the last N months that have snapshots, the newest one |
--keep-yearly N | for each of the last N years that have snapshots, the newest one |
--keep-within DURATION | every snapshot within the duration of the newest one, for example 30d or 1y6m |
--keep-tag TAG | every snapshot carrying the tag |
How the keep flags decide
Four points trip people up.
The unit is "periods that have a snapshot", not calendar days. --keep-daily 7 means the seven most recent days on which a snapshot exists. If you made no snapshots for a month and then run forget, the newest seven days that have snapshots are kept, including ones from last month. This is a safety property: a stopped backup job does not make the policy erase your history. It is also the reason --keep-daily 7 does not promise "the last seven calendar days".
Rules overlap, and they do not stack. The newest snapshot of the current day is the daily keeper, the newest of the current week is the weekly keeper, and they are often the same snapshot. A snapshot satisfying several rules is stored once. So 7 daily plus 4 weekly plus 6 monthly gives fewer than 17 snapshots.
A period's keeper is its newest snapshot. The weekly snapshot for a past week is the last one taken in that week, and the monthly snapshot is the last one in that month. Weeks follow ISO 8601 and start on Monday.
--keep-within is measured from the newest snapshot, not from today. If your last backup was ten days ago, "within 30 days" counts from that snapshot. This is again the safe direction.
A worked example you can check yourself
Suppose one snapshot a day with no gaps, and today is Monday 9 November 2026, with the usual policy:
restic forget --tag app --keep-daily 7 --keep-weekly 4 --keep-monthly 6
The rules select these snapshots:
| Rule | Selected snapshot dates |
|---|---|
| daily (7) | 3, 4, 5, 6, 7, 8, 9 November |
| weekly (4) | 9 Nov (this ISO week), 8 Nov, 1 Nov, 25 Oct (the last snapshot of each of the previous three weeks, which are Sundays here) |
| monthly (6) | 9 Nov, 31 Oct, 30 Sep, 31 Aug, 31 Jul, 30 Jun |
Now take the union. The weekly picks 9 Nov and 8 Nov are already in the daily list, and 9 Nov is also the monthly pick for November. The distinct survivors are the seven dailies, plus 1 Nov and 25 Oct from the weekly rule, plus 31 Oct, 30 Sep, 31 Aug, 31 Jul and 30 Jun from the monthly rule. That is 14 snapshots, not 17.
Worked example: snapshots selected vs kept
Read the same list as restore points. Last week you can restore any single day. Before that you have one point per week for three weeks, and then one per month back to the end of June. If you discover at the end of November that a table was damaged on 20 October, the closest points are 25 Oct and 31 Oct, and whatever you need from 20 October is not there. Policies are bets about how long a mistake stays hidden.
Rule of thumb: pick the policy from how long a mistake can stay hidden, then run
--dry-runto see the real dates.
Check before you delete
restic forget --tag app --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --dry-run
--dry-run (or -n) lists which snapshots would be kept and which removed, with the reason each is kept, and changes nothing. Run it every time you change the policy. It is also the fastest way to learn the semantics above on your own data.
What prune does, and why it costs
restic stores data in packs, each holding many deduplicated chunks. A snapshot is a list of references. After forget, some chunks may be referenced by no snapshot, but they sit inside packs that still hold live chunks, so nothing is freed yet.
restic prune does three things. It finds unreferenced data. It deletes packs where everything is unreferenced. And it repacks packs that are partly unreferenced by reading the live parts and writing them into new packs, then deleting the old ones. By default, repacking is skipped for the last bit of unused space (the --max-unused threshold, 5% by default), which is a trade of disk for effort.
Two consequences follow. Prune needs an exclusive lock on the repository, so it cannot run at the same time as a backup, and your schedule must separate them. And repacking moves data in both directions. On a local disk that is cheap. On object storage it means downloading and re-uploading, which costs time and, depending on the provider, egress and request charges.
For a small server with cloud storage, one split works well:
15 3 * * * root flock -n /run/backup.lock /opt/backup/backup.sh # dumps, backup, forget (no --prune), check
30 5 * * 0 root bash -c 'set -a; . /etc/backup.env; exec flock -n /run/backup.lock restic prune'
The idea is forget nightly and prune weekly. If your script runs forget --prune every night, that is also fine for modest repositories; look at how long prune takes and what your provider bills before you decide.
The flock in the prune line uses the same lock file your backup cron line uses, so the two can never run together. The kit's backup.sh runs forget --prune after each snapshot, which is the simple version; split the two only when prune becomes slow or costly.
Grouping: how a policy can quietly split
By default, forget applies the policy separately for each group of snapshots sharing a host and a set of paths. That is usually what you want when several machines share a repository. It has a sharp edge: if you change the list of backed-up paths, new snapshots belong to a new group, and the old group keeps its own policy applied to its own, now frozen, snapshots. You end up with the old group's last seven days retained forever, because nothing new arrives to push them out.
Look for it with:
restic snapshots --group-by host,paths
Keep the path list stable. For what to put in those paths and why the dump directory belongs in them, see a restic backup recipe for SQLite and Postgres. The kit's backup.sh always backs up the configured paths plus one dump directory, so the group is stable until you edit the config. When you do change paths deliberately, expect to clean up the old group once with explicit snapshot IDs:
restic forget a1b2c3d4 e5f6a7b8 --prune
Tags are a better dimension for filtering. Use --tag app on both backup and forget so a manual one-off snapshot with a different tag is never touched by the nightly policy.
Choosing a policy
Start from the question "how long can a mistake stay hidden?" and work out from there.
- A bad deploy or migration that you notice within a day.
--keep-daily 7is plenty. - A subtle data bug found in a weekly review. Add
--keep-weekly 8for two months of weekly points. - A customer who asks to recover something from last quarter.
--keep-monthly 12, and perhaps--keep-yearly 2if you have a reason. - Compliance or contract language about retention. Put the exact period in a
--keep-withinrule so the number in the policy matches the number in the document.
Weigh cost too. Deduplication means older snapshots mostly share chunks with newer ones, so the cost of a longer policy is the churn in your data, not a multiple of its size. A dump of a busy database changes more each night than a folder of uploads that rarely changes. Measure with restic stats after a month rather than guessing.
A sample that covers many cases:
restic forget --tag app \
--keep-within 14d \
--keep-weekly 8 \
--keep-monthly 12 \
--keep-yearly 2 \
--prune
Every snapshot for two weeks, then weekly for two months, monthly for a year, yearly for two. The nightly dump can be restored at any night in the last fortnight, which is the range where most mistakes get noticed.
Mistakes we see
- Running
forgetwith--keep-last 1. One snapshot is not a history. If last night's backup captured a damaged database, you have nothing older. - Never running
forget. The repository grows without bound, restores are not slower but bills are bigger, and check times creep up. - Running
prunewhile a backup runs. One of them fails with a lock error, and then someone reaches forrestic unlock. Schedule them apart and useflock. - Changing the path list and not noticing the group split. See above.
- Trusting the policy without
--dry-run. The first run after an edit should always be dry. - Assuming daily means calendar days. It means days that have snapshots. That is good for safety and bad for arithmetic.
- Pruning with append-only credentials. A key that cannot delete will fail. Run prune from a trusted machine, as described in 3-2-1 for one VPS.
FAQ
What is the difference between restic forget and restic prune?
forget removes snapshots from the repository's history according to your rules and frees no space. prune then deletes the data that no remaining snapshot needs. forget --prune does both in one command.
How many snapshots does keep-daily 7 keep-weekly 4 keep-monthly 6 leave?
Fewer than 17, because the rules overlap. With a daily snapshot and no gaps, the worked example above leaves 14. The exact number shifts with the day of the week and how recently a month ended.
Is it safe to run restic forget while a backup is running?
Do not overlap them. Removing snapshots and pruning both need an exclusive lock on the repository, and a backup holds a lock too, so one of the two will fail with a lock error. Keep them apart in your schedule with flock, and do not use restic unlock to force the issue. A --dry-run is safe at any time.
How do I see what the policy will delete before it does?
Add --dry-run to the forget command. The output lists each snapshot, whether it is kept, and the rule that keeps it. Nothing is changed.
Does a longer retention make the repository much bigger?
Not in proportion. Deduplication stores each unique chunk once, so growth follows how much your data changes, not how many snapshots you keep. Check restic stats after a few weeks.
The retention numbers in the VPS Deploy & Backup Kit are KEEP_DAILY, KEEP_WEEKLY and KEEP_MONTHLY in one config file, defaulting to 7, 4 and 6. The kit applies them with forget --tag kit --prune after every successful snapshot.