By design, when you delete an object in MinIO it is not immediately removed from disk — it is simply marked as deleted. In an installation where deletes happen constantly, that behaviour quietly eats capacity: usage keeps climbing even though nothing is being added, and the free space you expected after a cleanup never appears. Adding disks or nodes is not always an option, so the real answer is to make MinIO delete objects from disk for good.
Everything below uses mc, the MinIO Client, and assumes it is already configured. The examples use the prod alias and the test-bk bucket.
One thing to get out of the way first, because it wastes an afternoon: if your endpoint presents a certificate from a private CA, every mc admin call hangs rather than failing. There is no error, no timeout message, nothing — it simply sits there on the TLS verification. It looks exactly like a client/server version mismatch and it is not. Add --insecure:
mc --insecure admin info prod
First: is the bucket versioned?
This whole article only applies to a versioned bucket. On a bucket without versioning, a delete is a delete and the space comes back on its own. Check before you go any further:
[root@minio-prod-01 ~]# mc version info prod/test-bk
prod/test-bk versioning is enabled
If it says is un-versioned, your capacity problem is somewhere else — most likely erasure-code overhead or a bucket you have forgotten about.
There is a third answer: versioning is suspended. A bucket that had versioning on and then had it suspended still carries every version created while it was enabled, so treat it exactly like an enabled bucket for everything below.
What "marked as deleted" actually means
When an object is marked as deleted, it is not removed from disk — it is hidden from normal listings. Try to list a deleted object and it looks gone:
[root@minio-prod-01 ~]# mc ls prod/test-bk/file.pdf
[root@minio-prod-01 ~]#
List all versions with the --versions flag, though, and the truth shows up:
[root@minio-prod-01 ~]# mc ls --versions prod/test-bk/file.pdf
[2025-07-27 08:20:56 EEST] 0B STANDARD 6a2db261-173e-468a-89c4-c44f11d73c60 v2 DEL file.pdf
[2024-02-06 19:18:54 EET] 138KiB STANDARD 5997d305-af9e-4a2b-8c60-351c7f25c67d v1 PUT file.pdf
Two versions of the same file:
- v2,
DEL,0B— a delete marker. It occupies almost nothing itself; its only job is to tell MinIO the object should not appear in listings. - v1,
PUT, 138 KiB — the original upload. This is still on disk, and this is your missing capacity.
Multiply that by a few million objects and the arithmetic explains itself. Note also that the same thing happens on overwrites, not just deletes: every PUT over an existing key leaves the previous version behind as a noncurrent version.
How to delete objects for good: a lifecycle rule
The durable fix is a lifecycle rule: you tell MinIO, per bucket, how long to keep noncurrent versions and whether to clear delete markers, and its background scanner takes it from there. If a volume is already full and you need to delete objects on demand rather than at the scanner's pace, there is a manual purge at the end of this post — a supplement to the rule, not a replacement for it.
Step 1. Check for existing lifecycle rules
[root@minio-prod-01 ~]# mc ilm rule list prod/test-bk
mc: <ERROR> Unable to get lifecycle. The lifecycle configuration does not exist.
[root@minio-prod-01 ~]#
That Unable to get lifecycle. The lifecycle configuration does not exist. error is the expected answer on a bucket that has never had a rule. Worth running first: if a rule already exists, adding another one is not what you want — edit the one that is there.
Step 2. Add the rule
[root@minio-prod-01 ~]# mc ilm rule add prod/test-bk --noncurrent-expire-days 1 --expire-delete-marker
Lifecycle configuration rule added with ID `d23k55ooo92f1l7cj1eg` to prod/test-bk.
[root@minio-prod-01 ~]#
What the two flags do:
--noncurrent-expire-days 1— expire every non-current version of an object one day after it stops being current. This is the flag that actually frees space. One day is the minimum; a longer window is your undo buffer, so pick it deliberately.--expire-delete-marker— also remove delete markers once they are the only version left, so the object disappears entirely instead of leaving a tombstone.
On older mc builds the subcommand is mc ilm add rather than mc ilm rule add, with the same flags. If the command above is rejected outright, check mc --version before assuming the syntax is wrong.
Step 3. Verify the rule
[root@minio-prod-01 ~]# mc ilm rule list prod/test-bk
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ Expiration for latest version (Expiration) │
├──────────────────────┬─────────┬────────┬──────┬────────────────┬─────────────────────┤
│ ID │ STATUS │ PREFIX │ TAGS │ DAYS TO EXPIRE │ EXPIRE DELETEMARKER │
├──────────────────────┼─────────┼────────┼──────┼────────────────┼─────────────────────┤
│ d23k55ooo92f1l7cj1eg │ Enabled │ - │ - │ 0 │ true │
└──────────────────────┴─────────┴────────┴──────┴────────────────┴─────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Expiration for older versions (NoncurrentVersionExpiration) │
├──────────────────────┬─────────┬────────┬──────┬────────────────┬───────────────┤
│ ID │ STATUS │ PREFIX │ TAGS │ DAYS TO EXPIRE │ KEEP VERSIONS │
├──────────────────────┼─────────┼────────┼──────┼────────────────┼───────────────┤
│ d23k55ooo92f1l7cj1eg │ Enabled │ - │ - │ 1 │ 0 │
└──────────────────────┴─────────┴────────┴──────┴────────────────┴───────────────┘
[root@minio-prod-01 ~]#
Two tables, one rule ID. DAYS TO EXPIRE = 0 in the first table is not a mistake — that section covers the delete markers, which have no day count of their own; EXPIRE DELETEMARKER = true is the setting that matters there.
What --noncurrent-expire-days 1 actually costs you
Run the command above and you have not only freed space. You have also set your recovery window, and one day is a short one. With --expire-delete-marker on top, a delete or an overwrite becomes unrecoverable roughly 24 hours later — there is no separate trash, no soft-delete, nothing behind it.
That is a perfectly reasonable trade for a bucket of derived data. It is a bad surprise on a bucket somebody assumed was versioned and therefore safe.
There is a quick way to see which of the two you have. Compare the object count with the version count:
mc --insecure admin info prod
Divide versions by objects. A ratio close to 1.00 means versioning is enabled but is holding essentially no history: you are paying the metadata overhead of a versioned bucket and getting none of the protection. That is exactly what a one-day expiry produces once it has been running a while, and it is worth knowing deliberately rather than discovering it during a restore.
If you want real protection rather than just capacity, two things are worth knowing before you go further:
- Object Lock can only be enabled when the bucket is created. There is no way to retrofit it onto an existing bucket — it means creating a new one and migrating the data across.
- Object Lock and an aggressive noncurrent expiry pull in opposite directions. Locking objects means accepting the capacity cost that the expiry rule was adopted to avoid. Decide which problem you actually have before writing either.
Nothing happened. Now what?
This is the part that surprises people: the rule does not act immediately. MinIO applies lifecycle rules from its background scanner, which walks objects gradually so it does not disturb live traffic. The rule is only a policy; the scanner is the thing that goes out to delete objects on disk. On a bucket with millions of objects a full pass takes a while, and space comes back in stages rather than all at once.
So do not judge it after five minutes. Come back the next day and check the same object:
# the old version should be gone, not just hidden
[root@minio-prod-01 ~]# mc ls --versions prod/test-bk/file.pdf
# bucket usage, from MinIO's own accounting - see the caveat below
[root@minio-prod-01 ~]# mc du prod/test-bk
# and the actual filesystem, which is the number that pays the bills
[root@minio-prod-01 ~]# df -h /mnt/disk1
mc du is fine on a test bucket and a bad idea on a large one: it walks the entire namespace to produce that number, so on a bucket holding millions of objects it takes minutes and generates a very large number of LIST calls. On anything at that scale use mc admin info for the cluster totals instead, and save mc du for the single bucket you are actually investigating.
If usage drops in MinIO but df -h does not, the objects are gone and the problem has moved one layer down — see the note at the end.
If it still does not expire
- Object Lock or a retention policy on the bucket will hold versions past any lifecycle rule; that is the entire point of a WORM bucket, and it is the other side of the trade described above. Check with
mc retention info prod/test-bk. - Legal hold on individual objects does the same thing.
- A rule on the wrong bucket.
mc ilm rule listtakes the bucket, not the alias — running it againstprodalone tells you nothing.
Removing the rule
Rules are listed and removed by the ID shown above:
mc ilm rule rm --id d23k55ooo92f1l7cj1eg prod/test-bk
Removing the rule stops future expiry. It does not bring back anything that has already expired — noncurrent versions that the scanner has processed are gone from disk, permanently, with no recycle bin behind them. Set the day count to something you can live with before you enable the rule, not after.
Purging on demand, without waiting for the scanner
When a volume is already full, the scanner's deliberate pace is not much comfort. mc rm removes noncurrent versions directly:
# see what would go - --dry-run removes nothing
mc rm --recursive --versions --non-current --older-than 1d --dry-run prod/test-bk
# then, for real
mc rm --recursive --force --versions --non-current --older-than 1d prod/test-bk
--non-current restricts it to versions that are no longer current, so the live object survives, and --older-than keeps a window behind you. Run the dry run and read the list properly — --force --recursive --versions on a versioned bucket without --non-current is how people delete the data they meant to keep.
This is a one-off. Without a lifecycle rule the same backlog starts building again the next day.
Final note: the space may stop one layer lower
If this is a virtual machine on a thin-provisioned LUN, freeing the space inside MinIO is only the first of three steps. The guest filesystem, the VMware datastore and the array each have to be told separately — see how to reclaim space on the VMware datastore and the underlying physical storage, which picks up exactly where df -h stops being convincing.
If you are setting up the cluster in the first place, the layout these commands assume is the one in the step by step MinIO installation on Rocky Linux.
One note if you are building something new rather than maintaining an existing cluster: the community editions of minio and mc are no longer maintained and dl.min.io returns 410 Gone for their binaries. The current product is AIStor. None of the commands above change — mc ilm rule, mc version info, mc du and mc retention info are identical there.
One Comment