MongoDB Auto Increment: How to Get Sequential IDs
MongoDB has no built-in auto increment. There is no AUTO_INCREMENT keyword and no sequence object. The default _id is an ObjectId, a 12 byte value that is unique but not sequential in the way a counter is.
To get 1, 2, 3 you keep a counter document in its own collection. Before each insert you increment that counter with findOneAndUpdate and $inc. That single update is atomic, so two clients never receive the same number.
| Option | You get | Use it when |
|---|---|---|
| Default ObjectId | Unique, roughly time ordered | You do not need readable numbers |
| Counter collection | 1, 2, 3 with no gaps in normal use | You need an order number or invoice number |
| Counter in application memory | Fast, but wrong with more than one server | Never in production |
| Atlas trigger | Counter logic kept in the database | You already run on Atlas |
Here is the counter pattern in the MongoDB shell.
db.counters.insertOne({ _id: "orderId", seq: 0 }) function getNextSequence(name) { const doc = db.counters.findOneAndUpdate( { _id: name }, { $inc: { seq: 1 } }, { returnDocument: "after", upsert: true } ) return doc.seq } db.orders.insertOne({ _id: getNextSequence("orderId"), item: "Keyboard" })
Node.js With the Official Driver
The same call works in the Node.js driver. One detail changes between driver versions.
const counters = db.collection('counters') async function nextId(name) { const doc = await counters.findOneAndUpdate( { _id: name }, { $inc: { seq: 1 } }, { returnDocument: 'after', upsert: true } ) return doc.seq } await db.collection('orders').insertOne({ _id: await nextId('orderId'), item: 'Mouse' })
Driver version 6 and later returns the document itself. Older versions returned a wrapper, so you had to read result.value.seq. If doc.seq is undefined, print the whole result and check which shape you have.
Mongoose
Put the counter call in a pre-save hook. The model then behaves as if the field increments itself.
orderSchema.pre('save', async function (next) { if (!this.isNew) return next() const c = await Counter.findByIdAndUpdate( 'orderId', { $inc: { seq: 1 } }, { new: true, upsert: true } ) this.orderNumber = c.seq next() })
Keep the number in a normal field such as orderNumber. Leaving _id as an ObjectId is safer, because you can change the numbering later.
Java and C#
Both drivers expose the same operation with a return document option.
Document filter = new Document("_id", "orderId"); Document update = new Document("$inc", new Document("seq", 1L)); FindOneAndUpdateOptions opts = new FindOneAndUpdateOptions() .returnDocument(ReturnDocument.AFTER).upsert(true); long next = counters.findOneAndUpdate(filter, update, opts).getLong("seq");
var filter = Builders<BsonDocument>.Filter.Eq("_id", "orderId"); var update = Builders<BsonDocument>.Update.Inc("seq", 1); var opts = new FindOneAndUpdateOptions<BsonDocument> { ReturnDocument = ReturnDocument.After, IsUpsert = true }; var next = counters.FindOneAndUpdate(filter, update, opts)["seq"].AsInt64;
Use a 64 bit integer for the counter. A 32 bit field runs out at about two billion rows.
What Can Go Wrong
- Gaps appear. You take a number, then the insert fails. The number is gone. Numbering stays ordered but is not always continuous.
- The counter document is a hot spot. Every insert updates one document. At very high write rates that one document limits throughput.
- Sharding does not help this pattern. The counter lives on one shard, so all inserts wait on it.
- Sequential IDs leak information. A customer can read order number 4102 and estimate your volume.
- Two calls are not one transaction. The counter update and the insert are separate. Wrap both in a transaction if a gap is unacceptable.
If none of that matters to you, keep the ObjectId. It is generated by the client, needs no round trip, and never contends on a single document.
How to Prepare
- Test with two writers. Run two processes that insert at the same time. Confirm that no number repeats.
- Store the counter name as the
_id. One document per sequence keeps the update targeted and fast. - Always pass
upsert: true. The first call then creates the counter instead of returning null. - Know why the design is this way. ID generation is a common system design topic. Grokking System Design Fundamentals covers unique ID generation at scale.
- Expect this in interviews. Ask yourself out loud why MongoDB ships no sequence type. Good background is in what are two unique advantages of MongoDB and which DBMS is MongoDB.
- Cover the rest of the topic list. See which MongoDB interview questions to prepare.

GET YOUR FREE
Coding Questions Catalog

$123

$197

$72