Shrinking MongoDB storage on EBS: why it’s hard, and what your options actually are
If you’re running a self-hosted MongoDB on AWS, you’ve had this conversation. Someone archives a collection, drops an index, finishes a migration. On-disk size drops by a third. A week later, finance asks why the EBS bill hasn’t moved.
It hasn’t moved because nothing about deleting data in MongoDB gives storage back to EBS. The space is free inside MongoDB’s storage engine, which reuses freed space internally instead of shrinking files. The volume underneath is still the size it was.
And shrinking the volume? That’s the kind of thing most teams look at once, decide it isn’t worth the downtime, and quietly decide not to do it.
This post is about why. And what the actual options are. No pitch, just the tradeoffs.
What’s going on under the hood
MongoDB manages space inside its own data files. When you delete documents or drop a collection, that space becomes reusable internally. The underlying storage doesn’t shrink. New writes go into the reclaimed space before MongoDB grows the files further.
Fine for Mongo. Not as good for your EBS bill.
Before you assume the space is wasted, check what’s actually in there. A few things commonly account for on-disk size beyond live data:
- The snapshot history file (WiredTigerHS.wt) grows with minSnapshotHistoryWindowInSeconds.
- The journal holds modifications between checkpoints.
- Block compression defaults to snappy, which is fast but not aggressive. For write-heavy collections, switching the block compressor to zstd can give you 20–40% back before you think about resizing anything.
If you’ve ruled that out and there’s genuinely unused space: MongoDB has to release space back to the filesystem (which it doesn’t do on its own without compact), and the EBS volume itself needs to shrink. The first is solvable. The second is where most teams get stuck. AWS’s own docs are explicit: you can increase volume size, you can’t decrease it. The only supported path to a smaller volume is to create a new one and migrate the data across.
So any real shrink means creating a smaller volume, getting your data onto it, and swapping. The question is how much pain that involves.
Option 1: compact
The compact action defragments a collection’s data and indexes, and may release unused space to the operating system. “May,” because MongoDB doesn’t actually promise it will.
Three problems in practice:
- First: it holds a database-level lock on the node it runs on. On a primary, the node stops accepting writes until it finishes. On a large collection that can be hours. It’s workable if you run it on secondaries one at a time and step down the primary last, i.e. a full replica set rotation per compact cycle.
- Second: the space goes back to the filesystem, not to EBS. Compact does nothing about that.
- Third: it doesn’t always reclaim as much as you’d expect. On collections with heavy update workloads, fragmentation patterns leave less recoverable than you hoped. Worth a dry run on a restored snapshot before you commit to a maintenance window.
Useful when you want to reclaim filesystem-level space and keep the volume size. Not an answer if the goal is a smaller EBS bill.
Option 2: Logical dump and restore
Run mongodump, provision a smaller volume, mongorestore, swap the node in, resync, repeat.
That works. Also slow, error-prone at scale, and rebuilds every index from scratch on the restore side. On a large collection the index build can take longer than the dump itself. For a 2TB dataset: hours per node, across a weekend, coordinated across the replica set.
The upside: genuinely compacted, freshly-indexed dataset on a right-sized volume. Sometimes a small performance improvement on top.
The downside: a lot of work for a housekeeping task, and it doesn’t scale. If storage drifts again in six months, you do it again.
Teams that pick this usually do it once, bundled into a version upgrade or migration they were doing anyway.
Option 3: Snapshot, restore, swap
Snapshot the existing volume, create a new smaller volume, attach it to a fresh node, bring it into the replica set as a secondary, let it catch up, step down the primary, retire the old node.
This is the default for most experienced teams. It sidesteps compact’s lock contention and mongodump’s full rebuild. But there’s a catch: when you create a volume from a snapshot, the new volume must be at least as large as the original source volume. EBS won’t let you provision a smaller one directly.
So the real sequence is one of two things. Either restore to a same-size volume and copy the data down to a smaller one at the filesystem level (AWS itself suggests rsync for this). Or skip the snapshot and use initial sync from the replica set to populate a fresh smaller volume.
Initial sync is usually cleanest: it avoids the sizing constraint, produces a freshly-compacted dataset, and uses machinery you already trust. The cost is sync time. Many hours on a large dataset, with real load on the source node.
Workable for occasional, planned resizes. Not something you’d automate or run monthly.
Option 4: AWS Elastic Volumes
Worth addressing because it always comes up.
Elastic Volumes modifies a volume’s size, type, or IOPS without detaching it. Genuinely useful, and the right answer for growing a volume.
The AWS documentation is unambiguous: “You can only increase volume size. You can’t decrease volume size.” That’s the whole story for shrink.
Notice the pattern: every Elastic Volumes update is about growing faster. January 2026 lifted the six-hour cooldown, you can now do four modifications per volume in a rolling 24-hour window. Useful for scaling up under load. Does nothing for enabling shrink. Shrink is never on the list.
Option 5: Do nothing
A legitimate option. Also the one most teams end up at.
The reasoning is rational: the cost of getting a resize wrong on a production Mongo cluster is higher than the cost of the wasted storage. EBS is cheap per gigabyte. Downtime, resync storms, a failed step-down during peak traffic: those aren’t.
This stops being defensible when one of two things happens. The waste gets large enough that finance starts asking questions you can’t answer. Or the team grows past the point where “we don’t touch storage” is a policy a new hire will accept. Then you need an actual answer, not a shrug.
Option 6: Automate the swap
The pattern behind options 2 and 3 is the same: smaller volume, data sync, node swap. Several teams have built internal tooling for it. Scripts that pick a low-traffic window, rotate through the replica set, handle the step-downs, roll back if something looks wrong.
The right approach, finally. And for teams with strong platform engineering, achievable. The honest version of the story is that it takes longer than expected. The edge cases — failed initial sync, a node falling behind during cutover, a PVC binding issue on EKS, the fact that each EBS volume modification itself can take hours on a large volume — are where the engineering time goes. And the tooling has to be maintained as MongoDB versions, EBS behaviour, and team conventions drift.
Stated differently: you’re solving a problem that isn’t strictly yours. Every Mongo-on-EBS team is building roughly the same thing.
Where Datafy fits
Full disclosure: we make a tool that does this, automatically and seamlessly.
Datafy sits below MongoDB at the block layer. It presents a virtual volume to the filesystem and manages the underlying EBS volume size automatically, growing and shrinking based on actual usage. MongoDB sees a stable device. Mongo doesn’t know anything has changed. No compact, no dump-and-restore, no replica set rotation. Shrinking happens online, one node at a time. Rule-based and deterministic, i.e. you set the thresholds. And it can be removed from any volume at any time, returning you to a standard EBS volume with no Datafy dependency.
We’re not the only way to solve this. Options 1 through 4 all work, and a well-built version of option 6 works too. The argument is that the problem is common enough, and the stakes high enough, that building it yourselves is usually not worth it.
What to do on Monday
Measure it first. Look at mongostat, Mongo cache usage, on-disk size vs. logical data size and measure your waste. Check what your block compressor is set to — if it’s still snappy on a write-heavy workload, switching to zstd may close the gap before you touch anything else. The remaining gap is your upper bound on what’s reclaimable. You’ll get less than that in practice, but it tells you whether you’re looking at a 10% problem or a 40% problem.
Under 15%: leave it alone. The juice isn’t worth the squeeze.
15–30%: worth a scheduled rebuild the next time you’re doing a version upgrade or major maintenance. Bundle it in.
Over 30%: you have a real problem, and doing nothing is getting harder to defend. The question becomes whether you solve it manually (carefully planned options 2 or 3), or put something in place that keeps it from happening again.
Whatever you pick: test it on a restored snapshot first. The mistakes on this kind of work don’t show up during the operation. They show up three days later, when a query that used to be fast isn’t, and you’re trying to remember what you changed.