Deleting Documents Safely With deleteOne and deleteMany
Learners will remove single or multiple documents and discuss strategies for preventing accidental mass deletions.
Deleting Documents Safely With deleteOne and deleteMany is a free MongoDB Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the MongoDB Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Permanent Deletion in MongoDB
MongoDB provides two methods for removing documents:
- deleteOne(filter) — removes the first document that matches the filter and returns
{ acknowledged, deletedCount } - deleteMany(filter) — removes all documents matching the filter and returns the total
deletedCount
Unlike SQL's DELETE, MongoDB deletes are permanent and immediate—there is no transaction-based undo once the operation is acknowledged (unless you are within a multi-document transaction that has not yet committed). Always use deletes with careful, targeted filters.
// deleteOne: removes first matching document
const res1 = await db.collection('sessions').deleteOne(
{ token: expiredToken }
);
console.log(res1.deletedCount); // 0 or 1
// deleteMany: removes all matching documents
const res2 = await db.collection('sessions').deleteMany(
{ expiresAt: { $lt: new Date() } }
);
console.log(res2.deletedCount); // 0 or moreThe Danger of deleteMany({})
An empty filter {} in deleteMany deletes every document in the collection. This is equivalent to TRUNCATE TABLE in SQL and is one of the most dangerous accidental operations in MongoDB. Unlike SQL, there is no confirmation prompt—it simply deletes everything.
MongoDB does not provide a 'soft delete by default' mechanism—if you delete, data is gone. Best practices to prevent accidents: always double-check the filter before running a wide deleteMany, test the filter with countDocuments first, and consider requiring explicit confirmation in admin tools.
// DANGER: Deletes ALL documents in the collection!
db.users.deleteMany({});
// Before you run deleteMany - always verify your filter first:
// Step 1: Count what will be deleted
const count = await db.collection('users').countDocuments(
{ status: 'temp' } // Your intended filter
);
console.log('Will delete:', count, 'documents');
// Step 2: Only delete after reviewing the count
if (count > 0 && count < SAFE_LIMIT) {
await db.collection('users').deleteMany({ status: 'temp' });
}Soft Delete Pattern
Many applications implement a soft delete instead of a physical delete: add a deletedAt timestamp field (or isDeleted: true flag) and update it, rather than removing the document. The document stays in the database but is excluded from normal queries by filtering out deleted records.
Soft deletes enable: undelete/restore functionality, audit trails (who deleted what and when), delayed permanent removal (purge records older than 30 days), and compliance with data retention policies. The trade-off is that queries must always include the deletedAt: { $exists: false } filter, which is easy to forget.
// Soft delete: mark as deleted instead of removing
async function softDelete(collection, id) {
await db.collection(collection).updateOne(
{ _id: new ObjectId(id) },
{ $set: { deletedAt: new Date(), deletedBy: currentUserId } }
);
}
// All queries must exclude deleted records:
db.posts.find({ deletedAt: { $exists: false } });
// Purge records deleted more than 30 days ago:
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
db.posts.deleteMany({ deletedAt: { $lt: cutoff } });findOneAndDelete: Delete and Return
findOneAndDelete(filter, options) atomically finds a document, deletes it, and returns the deleted document in a single operation. This is useful when you need to process the deleted document's data—for example, claiming and removing a queued task.
The returned value is the document as it was before deletion. If no document matched the filter, null is returned. This atomic find-delete pattern eliminates the race condition between reading a document and deleting it.
// Atomic pop from a task queue
async function claimNextTask() {
const task = await db.collection('taskQueue').findOneAndDelete(
{ status: 'pending' },
{ sort: { priority: -1, createdAt: 1 } } // Highest priority, oldest first
);
if (!task) {
return null; // Queue is empty
}
// Process the task - it's already removed from the queue
await processTask(task);
return task._id;
}Cascading Deletes: Handling References
MongoDB does not enforce foreign key constraints like SQL databases. If you delete a parent document (e.g., a user), any documents in other collections referencing that user's _id (like orders, posts) continue to exist with a now-orphaned reference.
Handle cascading deletes explicitly in your application code or in a transaction: first delete the parent, then delete all related children. Always wrap cascading deletes in a transaction if atomicity matters—you do not want the parent deleted but child cleanup failing.
// Cascading delete with transaction
async function deleteUser(userId) {
const session = client.startSession();
try {
await session.withTransaction(async () => {
const id = new ObjectId(userId);
// Delete user
await db.collection('users').deleteOne({ _id: id }, { session });
// Delete all user's posts
await db.collection('posts').deleteMany({ authorId: id }, { session });
// Delete user's sessions
await db.collection('sessions').deleteMany({ userId: id }, { session });
});
console.log('User and all related data deleted');
} finally {
await session.endSession();
}
}TTL Indexes for Automatic Expiry
Rather than writing scheduled deletion jobs, MongoDB provides TTL (Time-To-Live) indexes that automatically delete documents after a set duration. A TTL index on a Date field deletes documents a specified number of seconds after the date stored in that field.
TTL indexes are perfect for: session tokens, email verification codes, temporary rate-limit records, job queue items, and any ephemeral data. The deletion runs in the background—MongoDB's TTL monitor thread checks every 60 seconds, so deletion is eventually consistent, not immediate.
// Create a TTL index: delete sessions 24 hours after createdAt
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 86400 } // 24h in seconds
);
// Sessions are automatically deleted 24h after their createdAt date
// No cron job needed!
// For user-defined expiry per document:
db.verificationCodes.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 } // Delete at the date stored in expiresAt
);
// Each document controls its own expiry: expiresAt: new Date(Date.now() + 3600000)Bulk Deletes and Performance
When deleting large numbers of documents, a single deleteMany with an empty (or wide) filter can lock the collection and impact other operations for the duration. For large bulk deletions in production:
- Batch in small chunks: delete 1000 documents at a time in a loop with a brief sleep between batches
- Filter by indexed field: always include an indexed field in the filter to avoid a collection scan
- Off-peak scheduling: run large purges during low-traffic hours
// Batched deletion of old logs (prevents locking)
async function purgeOldLogs(cutoffDate) {
const BATCH_SIZE = 1000;
let totalDeleted = 0;
while (true) {
const result = await db.collection('logs').deleteMany(
{ createdAt: { $lt: cutoffDate } },
{ limit: BATCH_SIZE } // Note: deleteMany has no built-in limit
// Instead, use find + deleteOne in a loop for true batching
);
totalDeleted += result.deletedCount;
if (result.deletedCount === 0) break;
await new Promise(r => setTimeout(r, 100)); // Brief pause between batches
}
return totalDeleted;
}Dropping a Collection vs deleteMany
To remove all documents from a collection you have two options:
db.collection.deleteMany({})— removes all documents but keeps the collection with its indexes and settings intact. Slower because each document deletion is individually logged in the oplog.db.collection.drop()— removes the entire collection including all indexes and metadata. Much faster for large collections because it deletes the storage files directly. The collection no longer exists after this.
Use drop() when you want to start fresh (e.g., resetting test data). Use deleteMany({}) when you want to keep the collection's index structure.
// Slow: deletes each document individually (keeps collection + indexes)
await db.collection('testData').deleteMany({});
// Fast: drops entire collection (collection and indexes are gone)
await db.collection('testData').drop();
// Recreate with indexes after drop:
await db.collection('testData').createIndex({ email: 1 }, { unique: true });
// Or just start inserting - collection recreates on first insertConfirm Before Bulk Delete Pattern
A defensive coding pattern for bulk deletions: always perform a dry-run count before executing the delete, and optionally require explicit confirmation if the count exceeds a safety threshold. This is especially important for admin scripts and migration jobs.
Log the deletion for audit purposes: record who triggered it, when, how many documents were deleted, and what filter was used. This creates a paper trail for compliance and makes post-incident investigation possible.
async function safeDeleteMany(collection, filter, options = {}) {
const { dryRun = false, maxAllowed = 1000 } = options;
// Always count first
const count = await db.collection(collection).countDocuments(filter);
console.log(`[DRY RUN] Would delete ${count} documents from ${collection}`);
if (dryRun) return { wouldDelete: count };
if (count > maxAllowed) {
throw new Error(`Safety: ${count} > max allowed ${maxAllowed}. Pass override to proceed.`);
}
const result = await db.collection(collection).deleteMany(filter);
console.log(`[DELETED] ${result.deletedCount} from ${collection}`);
return result;
}Archiving Instead of Deleting
An alternative to both hard delete and soft delete is archiving: move old documents to a separate archive collection rather than deleting them. This keeps the primary collection small and fast while preserving historical data for auditing or analytics.
Archive with the aggregation pipeline's $merge or $out stage, then delete from the primary collection. Atlas Data Federation can query archive collections on S3, enabling cost-effective long-term storage of high-volume historical data.
// Archive old orders then delete from primary collection
async function archiveOldOrders(beforeDate) {
// Step 1: Copy old orders to archive collection
await db.collection('orders').aggregate([
{ $match: { createdAt: { $lt: beforeDate }, status: 'completed' } },
{ $merge: { into: 'orders_archive', whenMatched: 'keepExisting' } }
]).toArray();
// Step 2: Delete them from primary collection
const del = await db.collection('orders').deleteMany({
createdAt: { $lt: beforeDate },
status: 'completed'
});
console.log('Archived and deleted:', del.deletedCount);
}Restoring Deleted Data From Backups
When accidental deletion occurs and soft delete is not in place, your recovery options depend on your backup strategy:
- MongoDB Atlas: point-in-time restore from continuous backup, down to a specific second
- Replica set oplog: replay oplog operations up to a specific timestamp (requires oplog to still contain the timeframe)
- mongodump backups: restore from the last dump, potentially losing hours of data
The key takeaway: no backup strategy compensates for careful deletion practices. The oplog window (typically 24-72 hours on Atlas) gives you a safety net, but prevention through careful filters and soft delete is always better than recovery.
// Atlas point-in-time restore via Atlas API or UI
// No mongosh command - done through Atlas console or API
// Using oplog to find what was deleted (expert operation)
// Connect to replica set and inspect oplog:
use local
db.oplog.rs.find({
op: 'd', // delete operation
ns: 'myapp.users', // your namespace
wall: { : new Date(Date.now() - 3600000) } // last hour
}).sort({ ts: -1 });
// Each entry shows the _id of the deleted documentQuick Check
Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.
Lesson Recap
In this lesson you learned: deleteOne and deleteMany are permanent—always verify your filter with countDocuments first and never run deleteMany({}) without explicit intent, soft delete (marking with deletedAt) is safer for user-facing content as it enables undelete, audit trails, and gradual purges, and TTL indexes enable automatic document expiry without cron jobs—ideal for sessions, verification codes, and temporary records. This completes the Updating and Deleting Documents course—next we move into data modeling with embedding versus referencing strategies.
Frequently asked questions
Is the “Deleting Documents Safely With deleteOne and deleteMany” lesson free?
Yes — the full text of “Deleting Documents Safely With deleteOne and deleteMany” is free to read here on the web, and the MongoDB Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the MongoDB Academy course, upgrade to CoddyKit PRO.
What will I learn in “Deleting Documents Safely With deleteOne and deleteMany”?
Learners will remove single or multiple documents and discuss strategies for preventing accidental mass deletions. You practise MongoDB Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start MongoDB Academy?
No prior experience is required. MongoDB Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Deleting Documents Safely With deleteOne and deleteMany” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this MongoDB Academy lesson?
Yes. Every MongoDB Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- updateOne and updateMany With $set and $unset
- Increment, Multiply, and Min/Max Operators
- Array Update Operators: $push, $pull, $addToSet
- Deleting Documents Safely With deleteOne and deleteMany