You meant to run deleteMany({ status: "cancelled" }). What ran was
deleteMany({}). It came back in eleven milliseconds and told you how
many documents it removed. There is no undo button, no rollback, no confirmation you can
take back. This post is the honest list of what actually works from here — and what people
wrongly assume will.
First: Stop Writing to That Collection
Every recovery route below either restores a copy or replays history. Both get harder the more the collection changes after the delete. If this is production, take the writers offline before you start reading recovery documentation. Five minutes of downtime is cheaper than a partial restore that silently overwrites the rows that survived.
What Actually Works
1. A backup — the only route that always exists
A mongodump archive, a managed-service snapshot, or a volume snapshot of the
data directory. Restore it somewhere other than the live cluster, extract only the
collection you lost, and copy the documents back. Never restore straight over a running
production database to recover one collection.
mongorestore --uri "mongodb://localhost:27017" \
--nsInclude "shop.orders" \
--nsFrom "shop.orders" --nsTo "shop.orders_restored" \
dump/
Landing it in orders_restored first means you compare before you merge. The
restriction is obvious and unavoidable: a backup is only as good as its age. If it ran at
03:00 and the delete happened at 16:40, everything written in between is not in it.
2. Point-in-time restore: backup plus oplog replay
This is the route that recovers the gap, and it is the one most people mean when they say
"we'll just replay the oplog". On a replica set, every write is recorded in the capped
oplog.rs collection. Restore the last backup, then replay the oplog forward and
stop just before the delete:
mongorestore --oplogReplay \
--oplogLimit <seconds_since_epoch>:<ordinal> \
dump/
Find the timestamp by reading the oplog around the incident — the delete entries carry
op: "d" and the namespace you lost:
db.getSiblingDB("local").oplog.rs.find({
ns: "shop.orders",
op: "d"
}).sort({ $natural: -1 }).limit(5) Managed platforms wrap this same mechanism in a UI — MongoDB Atlas calls it continuous cloud backup with point-in-time restore, available from the M10 tier upward. If you have it, use it: it is the same idea with the timestamp arithmetic done for you.
What Does Not Work — and Why People Think It Does
The oplog alone will not rebuild your documents
This is the single most expensive misconception in a MongoDB incident. A delete entry in the oplog looks roughly like this:
{ op: "d", ns: "shop.orders", o: { _id: ObjectId("...") } }
That is the whole record. The oplog stores what was removed, not what it
contained — the delete entry carries the _id and nothing else. You cannot
reconstruct a deleted document from the oplog; you can only learn which documents to go
fetch from a backup. The oplog is the timeline, the backup is the content, and you need both.
It is also capped. Once it wraps, the window is gone — which is why the very first instruction in this post is to stop writing.
There is no transaction to roll back
Multi-document transactions can be aborted, but only before they commit. A
deleteMany outside a transaction commits as it goes, and one that has returned
has already committed. There is no db.collection.undelete(), and no server-side
recycle bin waiting to be emptied.
A dropped collection takes its indexes with it
drop() removes documents and index definitions, and
dropDatabase() removes every collection in one call. Both are cheap to type and
total in effect, which is a bad combination. Treat them as a different class of operation
from a delete with a filter.
Prevention, Because Recovery Is Always Worse
- Run the filter as a
find()first. Same filter, same collection,countDocuments()on the end. If the number surprises you, the filter is wrong — and you found out for free. - Reach for
deleteOnebeforedeleteMany. When you want exactly one document gone, say so. A wrong filter then costs one row, not the table. - Never run a partial selection of a delete. Highlighting the first line of a two-line statement gives you a complete, valid, unfiltered command — this is a real hazard in every SQL and shell editor, and we wrote up how we handled it.
- Verify the restore path before you need it. A backup nobody has ever restored is a hypothesis, not a backup.
What Sutido Does About It
None of the above is comfortable at 16:40 on a Friday, so Sutido keeps a local safety net
underneath it. Before deleteOne, deleteMany or
findOneAndDelete runs, the client reads the documents that filter matches and
snapshots them into a local SQLite changelog. Document edits made in the grid are captured
the same way — the version before the save is kept. When it turns out you removed the wrong
thing, open View → Document Changelog and restore, document by document.
The limits, stated plainly, because a safety net you misjudge is worse than none:
- A capture stops at 1000 documents, and the delete still proceeds.
- The changelog list shows the 200 most recent entries.
- It is local to your machine — one person's client, not a cluster-wide journal.
-
drop(),dropDatabase()and deletes issued throughrunCommandare not journaled at all. Those raise a confirmation dialog naming what is about to be destroyed instead, because there is no second chance to offer. - SQL engines have no equivalent journal anywhere in the industry. There, Sutido warns before the statement is sent rather than promising a recovery it cannot deliver.
It is a safety net for the "oh no" moment, not a backup strategy. Keep the backups. But the incident that never reaches the backup is the cheapest one you will ever have.
Delete With a Net
Sutido snapshots MongoDB deletes locally before they run — and warns before the ones it cannot.
Download Now