MongoDBdeleteOne & deleteMany Deep Dive

Delete

MongoDB gives you deleteOne() and deleteMany() to remove documents matching a filter, plus drop() to remove an entire collection in one shot. Deletes are permanent — there's no undo — so filter precision and, where it matters, soft deletes deserve real attention.

deleteOne()

Removes the first document matching the filter. If the filter matches several documents, only one is removed — which one is not something you should rely on unless the filter is unique.

deleteOne basics

JS
const result = db.users.deleteOne({ email: "alice@example.com" })

result.deletedCount   // 0 or 1
result.acknowledged   // true
deleteMany()

Removing every matching document

JS
const result = db.sessions.deleteMany({ expiresAt: { $lt: new Date() } })
result.deletedCount   // number of expired sessions removed
Warning
An **empty filter** — db.collection.deleteMany({}) — removes every document in the collection. This is a common copy-paste accident. Always double-check the filter with a find() or countDocuments() using the exact same filter before running the delete.

Preview before you delete

JS
// Step 1 — see exactly what would be removed
db.sessions.countDocuments({ expiresAt: { $lt: new Date() } })
db.sessions.find({ expiresAt: { $lt: new Date() } }).limit(5)

// Step 2 — only then run the delete with the identical filter
db.sessions.deleteMany({ expiresAt: { $lt: new Date() } })
Deleting All Documents vs Dropping the Collection

deleteMany({})

collection.drop()

Removes

All documents

The entire collection, including its documents AND indexes

Indexes

Preserved

Dropped along with the collection

Speed on huge collections

Slower — deletes are logged per document

Very fast — a metadata-level operation

Collection still exists after?

Yes, empty

No — must be recreated (implicitly, on next insert) if needed

drop() removes the collection entirely

JS
db.tempImport.drop()   // true if it existed and was dropped
// The collection, its documents, and ALL of its indexes are gone.
// A subsequent insert recreates the collection with NO indexes except the default _id one.
Note
If you only want to empty a collection and immediately reuse the exact same indexes, deleteMany({}) is usually simpler than drop() + re-creating every index by hand. If you don't care about the existing indexes, drop() is dramatically faster on large collections.
Soft-Delete Pattern

Many applications never physically delete records that matter for auditing, analytics, or recovery — instead they flag documents as deleted and filter them out of normal queries.

Soft delete via a flag + timestamp

JS
// "Delete" — really just flags the document
db.orders.updateOne(
  { _id: 42 },
  { $set: { deletedAt: new Date(), status: "deleted" } }
)

// Normal application queries always exclude soft-deleted documents
db.orders.find({ deletedAt: { $exists: false } })

// A partial index keeps the "active documents" query fast without
// indexing the (usually much smaller) deleted set
db.orders.createIndex(
  { customerId: 1 },
  { partialFilterExpression: { deletedAt: { $exists: false } } }
)
Tip
Soft deletes trade storage for recoverability and audit history. Combine them with a scheduled job (or a TTL index on deletedAt) that permanently purges soft-deleted documents after a retention window, so the "deleted" set doesn't grow forever.
TTL-Based Deletion

For data that should expire automatically — sessions, verification tokens, cached results — a TTL (time-to-live) index lets MongoDB's background process delete documents for you once a date field passes a threshold, with no application code required. See the dedicated TTL index page for the full mechanics.

TTL index — auto-expiring documents

JS
// Documents are removed ~60 seconds after "createdAt"
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 })
Write Concern for Deletes

Just like inserts and updates, delete operations accept a writeConcern option controlling how many replica set members must acknowledge the delete before the driver treats it as durable.

Write concern on a delete

JS
db.orders.deleteOne(
  { _id: 42 },
  { writeConcern: { w: "majority", wtimeout: 5000 } }
)
Warning
A delete acknowledged with the default write concern can still be rolled back if the primary steps down before the write replicates. For deletes you must never lose track of (billing records, compliance-sensitive data), use w: "majority" and prefer soft deletes over physical deletes in the first place.
Example Session
test> db.sessions.countDocuments({ expiresAt: { $lt: new Date() } })
14

test> db.sessions.deleteMany({ expiresAt: { $lt: new Date() } })
{ acknowledged: true, deletedCount: 14 }

test> db.tempImport.drop()
true

test> show collections
orders
products
sessions
users
  • deleteOne() — removes at most one matching document.

  • deleteMany() — removes every matching document; an empty filter deletes everything.

  • drop() — removes the entire collection AND its indexes; much faster for clearing large collections.

  • Soft delete (a deletedAt flag) when you need auditability or recovery.

  • TTL index when documents should expire automatically after a fixed time window.