E11000 Duplicate Key Error in MongoDB (and dup key: null)

Posted by Kyle Hankinson August 2, 2026


Somewhere in your logs is a write failure that makes no sense:

E11000 duplicate key error collection: shop.users index: email_1 dup key: { email: null }

You never inserted null into email. You may never have inserted email at all. That is precisely the problem, and it is the most-viewed MongoDB error on Stack Overflow for a reason: the root cause lives in an index you may not remember creating, and the intuitive fixes make it worse. This article reproduces the error on MongoDB 8.2, explains the null trap, and works through the fixes in the order you should try them.

Reading the message

E11000 is MongoDB's unique constraint violation, and everything you need is packed into the text:

  • collection: shop.users is the namespace (database shop, collection users).
  • index: email_1 is the index that rejected the write. The name encodes the key pattern: field email, ascending. Compound indexes chain them, like email_1_tenant_1.
  • dup key: { email: null } is the value that already exists in the index.

The error code is 11000, which matters when you handle it in application code. When the duplicate is a real value, the message is doing its job honestly:

E11000 duplicate key error collection: shop.users index: email_1 dup key: { email: "a@b.com" }

The confusion starts when that value is null and your code never wrote one.

The null trap, in four lines

Unique indexes in MongoDB index every document in the collection, whether or not the document contains the indexed field. A document with no email field is stored in the index under the key null. Unique means at most one document per key, so at most one document in the whole collection may omit the field. The second one collides:

db.users.createIndex({ email: 1 }, { unique: true })

db.users.insertOne({ name: "Ana" })   // fine: occupies the null slot
db.users.insertOne({ name: "Ben" })   // E11000 ... dup key: { email: null }

Verified on MongoDB 8.2.11 and 7.0.37; the error text is identical on both. Very old servers (the 2.x and 3.x era) printed dup key: { : null } without the field name, which is the variant fossilized in the top Stack Overflow answers.

This bites hardest in two situations. First, adding a unique field to an existing collection: every pre-existing document lacks the field, so the second write after the index appears fails. Second, stale indexes: you had unique: true on a field in a Mongoose schema, renamed or removed the field, and the index quietly stayed behind. Mongoose's unique option is not a validator; it is an instruction to create an index once, and deleting it from the schema drops nothing on the server.

Find the index you forgot about

The index named in the error is the whole diagnosis. List them:

db.users.getIndexes()
[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  { v: 2, key: { email: 1 }, name: 'email_1', unique: true }
]

Anything with unique: true on a field your current code no longer populates is your suspect. If you browse your data in a GUI, this check is a glance rather than a shell command; SQLPro for MongoDB shows every collection's index list with unique badges in its collection inspector, right next to the sampled fields and their BSON types, which makes a stale email_1 stand out immediately.

The fixes, ranked

1. If the index is obsolete, drop it. The rename-and-leftover-index case needs nothing cleverer:

db.users.dropIndex("email_1")

2. If you want uniqueness only when the field exists, use a partial index. This is the modern fix (available since MongoDB 3.2). The index only includes documents matching a filter, so documents without the field are simply not indexed and never collide:

db.users.dropIndex("email_1")
db.users.createIndex(
  { email: 1 },
  { unique: true, partialFilterExpression: { email: { $exists: true } } }
)

db.users.insertOne({ name: "Ana" })                      // ok
db.users.insertOne({ name: "Ben" })                      // ok now
db.users.insertOne({ name: "Cy", email: "a@b.com" })     // ok
db.users.insertOne({ name: "Di", email: "a@b.com" })     // still E11000

All four results verified on 8.2. Real duplicates still fail, missing fields sail through. One caveat: an explicit email: null value satisfies $exists: true, so if your application writes literal nulls, filter on type instead: partialFilterExpression: { email: { $type: "string" } }.

3. The sparse index is the legacy version of the same idea. { unique: true, sparse: true } also skips documents missing the field (verified: both no-email inserts succeed). It predates partial indexes, only supports "field is absent" as its condition, and the manual recommends partial indexes instead because sparse indexes can be silently ignored for sorts and return incomplete results when used to cover queries. Reach for it only on servers older than 3.2, which in 2026 should be nobody.

4. Or actually populate the field. If every document should have an email, the fix is a backfill, not an index change. The error is doing you a favor.

Before you create a unique index: find existing duplicates

Building a unique index on a collection that already contains duplicates fails mid-build with the same E11000, naming whichever duplicate it hit first. Check first with a $group:

db.contacts.aggregate([
  { $group: { _id: "$email", count: { $sum: 1 }, ids: { $push: "$_id" } } },
  { $match: { count: { $gt: 1 } } }
])
{ _id: null, count: 2, ids: [ ObjectId('...776'), ObjectId('...777') ] }
{ _id: 'a@b.com', count: 2, ids: [ ObjectId('...773'), ObjectId('...774') ] }

Note the first row: documents missing the field group under _id: null, exactly mirroring how the index will treat them. That row predicts a dup key: { email: null } failure before you ever build the index. Decide per group which _ids to keep, delete the rest, then create the index. The same keep-one-per-group reasoning applies in relational databases, where we covered it in how to find and delete duplicate rows in SQL.

The upsert race, briefly

There is one variant where E11000 appears without any modeling mistake. Two concurrent upserts filter on the same key, both find nothing, and both proceed to insert; one wins, the other gets E11000. Since MongoDB 4.2 the server retries most of these internally, but the race can still surface under load or when the filter does not exactly match the unique key. The fix is in application code: treat error code 11000 on an upsert as "someone else inserted first" and retry the operation once. On the second pass the document exists and the upsert takes its update path.

E11000 always tells the truth about one thing: some unique index rejected a key. When the key is a value, find the other document holding it. When the key is null, stop looking at your data and start looking at getIndexes(), because the collision is between two documents that never mention the field at all.


Tags: MongoDB

About the authorKyle Hankinson is the founder and sole developer of SQLPro Studio and the Hankinsoft Development suite of database tools. He has been building native macOS and iOS applications since 2010.

Try SQLPro Studio — A powerful database manager for MySQL, PostgreSQL, Microsoft SQL Server, SQLite, Oracle, and Snowflake. Available on macOS, iOS, and Windows.

Download Free Trial View Pricing Compare