Fix MongoDB E11000 Duplicate Key Error (Mongoose Guide)
On this page
- 1. A real duplicate: tell the user nicely
- 2. The dup key is null
- 3. An old index from a field you removed
- 4. unique: true seems to do nothing, then errors later
- How a unique index works
- Unique combinations with compound indexes
- Case-insensitive uniqueness
- E11000 during upserts
- One error handler for all duplicate errors
- Checking indexes in MongoDB Atlas and Compass
- Frequently asked questions
- Oh hi there 👋It’s nice to meet you.
- Sign up to receive awesome content in your inbox.
A user tries to sign up and your API throws:
MongoServerError: E11000 duplicate key error collection: shop.users
index: email_1 dup key: { email: "aarav@example.com" }
MongoDB is protecting a unique index. Something already uses that value, and the write would create a second one. The message tells you everything you need:
| Part of the message | Meaning |
|---|---|
collection: shop.users |
Database shop, collection users |
index: email_1 |
The unique index that blocked the write |
dup key: { email: "…" } |
The value that already exists |
Now match it to one of the four situations below.
1. A real duplicate: tell the user nicely
Someone tries to register with an email that is taken. That is the index doing its job. Don’t send a 500 error; catch code 11000 and return 409 Conflict:
app.post('/api/users', async (req, res, next) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (err) {
if (err.code === 11000) {
const field = Object.keys(err.keyValue || {})[0] || 'field';
return res.status(409).json({ message: `That ${field} is already in use.` });
}
next(err);
}
});
Don’t check “does this email exist?” first and then insert. Two requests at the same moment can both pass the check. The unique index is the only reliable guard, so let it reject and handle the error.
2. The dup key is null
E11000 duplicate key error ... index: phone_1 dup key: { phone: null }
A normal unique index treats a missing field as null, and only one document can have null. The second user without a phone number fails. Index only documents that actually have the field, with a partial index:
const userSchema = new Schema({
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
phone: { type: String },
});
userSchema.index(
{ phone: 1 },
{ unique: true, partialFilterExpression: { phone: { $type: 'string' } } }
);
Then drop the old index so the new one can be built (see the next section).
3. An old index from a field you removed
You renamed username to handle, but the error still mentions username_1. Removing a field from your schema does not remove its index from the database. List and drop it in mongosh:
db.users.getIndexes()
db.users.dropIndex("username_1")
In Mongoose you can also compare your schema with the database and remove indexes the schema no longer defines with await User.syncIndexes(). Run it carefully on production data, ideally from a one-off script rather than on every startup.
4. unique: true seems to do nothing, then errors later
In Mongoose, unique: true is not a validator. It asks Mongoose to build a unique index. If the collection already contains duplicates, the index build fails, so duplicates slip in until a later write hits a different index or you fix the data. Find existing duplicates with an aggregation:
db.users.aggregate([
{ $group: { _id: { $toLower: "$email" }, count: { $sum: 1 }, ids: { $push: "$_id" } } },
{ $match: { count: { $gt: 1 } } }
])
Clean them up, then build the index. Adding lowercase: true and trim: true to email fields stops Aarav@Example.com and aarav@example.com from being treated as different users.
New to aggregations? Read MongoDB aggregation for SQL developers, or paste a SQL query into the SQL to MongoDB converter.
How a unique index works
An index is a sorted list of values with pointers to the documents that contain them, like the index at the back of a book. A unique index adds one rule: each value may appear only once. Every insert and update checks the index first. If the new value is already there, MongoDB rejects the write with E11000 before anything is saved.
Two consequences are worth remembering. First, the check is atomic: even if a thousand requests arrive at the same millisecond, only one can win. That is why the index, not an “if it exists” query, is the right protection. Second, the rule applies to every write path, including scripts, admin tools and imports, not only your API.
Unique combinations with compound indexes
Often a value only needs to be unique together with another value. A user may review many products, and a product may have many reviews, but one user should review one product only once. Use a compound unique index:
const reviewSchema = new Schema(
{
product: { type: Schema.Types.ObjectId, ref: 'Product', required: true },
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
rating: { type: Number, min: 1, max: 5, required: true },
text: { type: String, maxLength: 2000 },
},
{ timestamps: true }
);
reviewSchema.index({ product: 1, user: 1 }, { unique: true });
A second review by the same user for the same product now fails with E11000, and the error’s keyValue contains both fields. Your API can turn that into a helpful message such as “You have already reviewed this product. Edit your review instead.”
Other common examples: one slug per blog ({ blog: 1, slug: 1 }), one membership per user per team ({ team: 1, user: 1 }), and one invoice number per company.
Case-insensitive uniqueness
A normal unique index treats Aarav@Example.com and aarav@example.com as different values. For emails, the simplest fix is to store them in lowercase with Mongoose’s lowercase: true, as shown earlier. For values where you must keep the user’s original capitalization, such as usernames, create the index with a case-insensitive collation:
userSchema.index(
{ username: 1 },
{ unique: true, collation: { locale: 'en', strength: 2 } }
);
// Queries must use the same collation to use this index
await User.findOne({ username: 'Aarav' }).collation({ locale: 'en', strength: 2 });
Strength 2 compares letters and accents but ignores case. Remember to pass the same collation in queries, or MongoDB won’t use the index for them.
E11000 during upserts
An upsert updates a document if it exists and inserts it if it doesn’t. When two requests upsert the same new key at the same moment, both can decide “it doesn’t exist yet” and both try to insert. One succeeds; the other gets E11000. This is expected behavior, and the fix is simply to retry once, because the second attempt will find the document and update it:
async function upsertWithRetry(filter, update, retries = 1) {
try {
return await Stats.findOneAndUpdate(filter, update, { upsert: true, new: true });
} catch (err) {
if (err.code === 11000 && retries > 0) return upsertWithRetry(filter, update, retries - 1);
throw err;
}
}
await upsertWithRetry({ day: '2026-11-11', page: '/pricing' }, { $inc: { views: 1 } });
For this to work, the fields in the filter must be covered by a unique index. Without the index, the race silently creates two documents instead of throwing an error.
One error handler for all duplicate errors
Instead of checking err.code === 11000 in every route, handle it once in your Express error handler and turn it into a consistent response:
const FRIENDLY = {
email: 'That email is already registered.',
slug: 'That URL is already used by another post.',
'product,user': 'You have already reviewed this product.',
};
app.use((err, req, res, next) => {
if (err.code === 11000) {
const fields = Object.keys(err.keyValue || {});
const message = FRIENDLY[fields.join(',')] || `Duplicate value for ${fields.join(', ')}.`;
return res.status(409).json({ message, fields });
}
if (res.headersSent) return next(err);
console.error(err);
res.status(err.status || 500).json({ message: 'Something went wrong' });
});
With Express 5, errors from async route handlers reach this handler automatically, so routes can simply await Model.create() without any try/catch.
Checking indexes in MongoDB Atlas and Compass
You don’t need the shell to see your indexes. In MongoDB Compass or the Atlas web interface, open the collection and select the Indexes tab. It lists every index, whether it is unique, its size and how often it is used. Indexes with zero usage after weeks of traffic are good candidates for removal, because every index slows down writes a little and uses memory.
Frequently asked questions
What does E11000 mean?
It is MongoDB’s error code for a duplicate key: a write would put the same value twice into a field covered by a unique index.
Which HTTP status should my API return for a duplicate?
409 Conflict, with a message telling the user which value is already taken. Don’t reveal more than needed; for login-related forms, a generic message is safer.
Can I make MongoDB ignore duplicates in insertMany?
Pass { ordered: false }. MongoDB then inserts every valid document and reports the duplicates as errors at the end instead of stopping at the first one.
Does Mongoose’s unique option validate before saving?
No. It only asks MongoDB to create a unique index. The check happens in the database when you save, which is why you handle E11000 in your error handler rather than expecting a Mongoose validation error.
Will dropping and recreating an index lose data?
No. Dropping an index removes only the index, never documents. Rebuilding it on a large collection can take time and add load, so do it during a quiet period.




