MongoDB "E11000 duplicate key error": read which index, and which value
Fix MongoDB "E11000 duplicate key error": read the index and value in the message, solve dup key null with a partial index, clean up duplicates, handle upsert races.
by OhSudo ·
Checked on real software: MongoDB 7 · November 2026How we check
MongoServerError: E11000 duplicate key error collection: shop.users index: email_1 dup key: { email: "ana@example.com" }A unique index refused the write, because another document already has that value. The message has everything you need:
| Part | Here | Tells you |
|---|---|---|
collection |
shop.users |
Database and collection |
index |
email_1 |
Which unique index said no |
dup key |
{ email: "ana@example.com" } |
The value that’s already taken |
Which case you have depends on the value and the index.
TL;DR
db.users.getIndexes() // which indexes are unique?db.users.find({ email: "ana@example.com" }) // who already has it?If it says dup key: { email: null }, it’s documents without that field: use a partial index (section 2).
1. A real duplicate
The value exists, so decide what should happen in your application: tell the user, or update instead of inserting. Catch it by its code, not by matching the text:
try { await users.insertOne({ email });} catch (err) { if (err.code === 11000) { return res.status(409).json({ error: 'Email already registered' }); } throw err;}If index says _id_, then the same _id was inserted twice. A common way is re-running an import. mongoimport --mode=upsert or mongorestore --drop are made for that, so choose what fits your data.
2. dup key: null
This one surprises people:
E11000 duplicate key error collection: shop.users index: email_1 dup key: { email: null }A unique index treats a missing field as null. The first document without an email is accepted, and the second is a duplicate. To apply the rule only where the field exists, use a partial index:
db.users.dropIndex("email_1")db.users.createIndex( { email: 1 }, { unique: true, partialFilterExpression: { email: { $type: "string" } } })The older sparse: true option works similarly for one field. But partial indexes are more flexible, and MongoDB recommends them.
If email should always be there, the real bug is the code inserting documents without one. Add a schema rule, so that it fails with a clear error instead:
db.runCommand({ collMod: "users", validator: { $jsonSchema: { required: ["email"] } }})The same null shows up after a field was renamed in the code but not in the index: the index still expects the old name, so every new document has it “missing”. Compare getIndexes() with what your code writes.
3. It fails while creating the index
MongoServerError: Index build failed: 6a1c…: Collection shop.users ( 3f2a… ) :: caused by :: E11000 duplicate key error collection: shop.users index: email_1 dup key: { email: "ana@example.com" }The collection already has duplicates, so the unique index can’t be built. Find them all:
db.users.aggregate([ { $group: { _id: "$email", count: { $sum: 1 }, ids: { $push: "$_id" } } }, { $match: { count: { $gt: 1 } } }], { allowDiskUse: true })Decide which to keep, then remove the others. Back up first, as this deletes data:
mongodump --db=shop --collection=users --out=backup/db.users.deleteOne({ _id: ObjectId("66f0c2a1e4b0a1b2c3d4e5f6") })Then create the index again. Mongoose users, note that unique: true in a schema isn’t a validator: it only asks for this index, and with existing duplicates it fails quietly in the background unless you listen for it.
4. Upserts that collide
Two requests at the same moment can both find nothing and both insert, even with an upsert:
db.users.updateOne({ email }, { $set: { lastSeen: new Date() } }, { upsert: true })The unique index is doing its job, keeping one winner. Don’t remove it, since that’s how real duplicates get in. Retry on 11000: the second try finds the document and updates it.
async function touch(email) { for (let attempt = 0; attempt < 3; attempt++) { try { return await users.updateOne({ email }, { $set: { lastSeen: new Date() } }, { upsert: true }); } catch (err) { if (err.code !== 11000 || attempt === 2) throw err; } }}The filter must match the unique index. If you upsert on { _id: x } and set an email someone else already has, no retry helps: that’s a real duplicate.
5. Bulk inserts
By default insertMany stops at the first duplicate. To insert everything else and report the failures, turn off ordering:
try { await users.insertMany(docs, { ordered: false });} catch (err) { console.log(`${err.insertedCount} inserted, ${err.writeErrors.length} duplicates`);}Warning
Don’t drop a unique index just to make the error go away. It’s often the only thing guaranteeing that one email has one account, and code can’t guarantee that alone, because of the race in section 4.
Shell, we have a problem!
Similar, but different: connect ECONNREFUSED 127.0.0.1:27017 means you never reached the server: see the MongoDB connection article. And on PostgreSQL, the same situation is duplicate key value violates unique constraint.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.