iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Upserts

An upsert is “update if exists, insert if not.” updateOne + { upsert: true }. Atomic, race-free, the foundation of counters, idempotency, and cache patterns.

updateOne upsert, replaceOne, findAndModify

EXAMPLE
// 1) Basic upsert
await db.users.updateOne(
    { email: 'ada@example.com' },
    { $set: { name: 'Ada', lastSeen: new Date() } },
    { upsert: true },
);
// If 'ada@example.com' exists → updates. Otherwise → inserts.

// Returns:
// { matchedCount, modifiedCount, upsertedCount, upsertedId }

// 2) Set only on insert — $setOnInsert
await db.users.updateOne(
    { email: 'ada@example.com' },
    {
        $set:         { lastSeen: new Date() },          // every time
        $setOnInsert: { createdAt: new Date(), id: crypto.randomUUID() },  // only when inserting
        $inc:         { visits: 1 },
    },
    { upsert: true },
);
// createdAt is set on first call; not touched on subsequent calls.

// 3) Counter pattern — atomic, race-free
await db.counters.updateOne(
    { name: 'orders' },
    { $inc: { value: 1 } },
    { upsert: true },
);

// Get the new value:
const { value } = await db.counters.findOneAndUpdate(
    { name: 'orders' },
    { $inc: { value: 1 } },
    { upsert: true, returnDocument: 'after' },
);
console.log(value.value);                       // new counter value

// 4) Idempotency — same request many times, single insert
await db.events.updateOne(
    { idempotencyKey: req.headers['idempotency-key'] },
    {
        $setOnInsert: {
            payload: req.body,
            createdAt: new Date(),
            status: 'received',
        },
    },
    { upsert: true },
);
// Whether the request is duplicated or not, the document exists exactly once.

// 5) replaceOne — full replace, optionally upsert
await db.users.replaceOne(
    { _id: userId },
    { name: 'Ada', email: 'a@x.com', updatedAt: new Date() },
    { upsert: true },
);
// Replaces ALL fields except _id with the new doc.
// Different from updateOne: replaceOne doesn't preserve fields not in the new doc.

// 6) findOneAndUpdate — atomic read + write
const doc = await db.users.findOneAndUpdate(
    { _id: userId },
    { $set: { lastSeen: new Date() }, $inc: { logins: 1 } },
    { returnDocument: 'after', upsert: true },
);
// Returns the updated (or newly-created) document atomically.

// returnDocument: 'before' returns the pre-update doc; useful for state-machine transitions.

// 7) Concurrent upserts — race-safe with unique index
await db.users.createIndex({ email: 1 }, { unique: true });

// Two concurrent processes both upserting the same email:
await Promise.all([
    db.users.updateOne({ email: 'a@x.com' }, { $set: { name: 'Ada' } }, { upsert: true }),
    db.users.updateOne({ email: 'a@x.com' }, { $set: { name: 'Cy' } }, { upsert: true }),
]);
// Mongo guarantees one insert + one update. Final state: one doc with name from whichever ran second.
// Without the unique index, you can get duplicate documents under contention.

// 8) Catch duplicate-key on concurrent insert
try {
    await db.users.updateOne(
        { email: 'a@x.com' },
        { $setOnInsert: { name: 'Ada' } },
        { upsert: true },
    );
} catch (e) {
    if (e.code === 11000) {
        // Duplicate-key during upsert race — retry once
        await db.users.updateOne(
            { email: 'a@x.com' },
            { $set: { name: 'Ada' } },          // existing doc; just update
        );
    } else throw e;
}

// 9) Bulk upsert
await db.products.bulkWrite([
    {
        updateOne: {
            filter: { sku: 'A-100' },
            update: { $set: { stock: 50, price: 9.99 } },
            upsert: true,
        },
    },
    {
        updateOne: {
            filter: { sku: 'A-101' },
            update: { $set: { stock: 30, price: 19.99 } },
            upsert: true,
        },
    },
], { ordered: false });

// 10) Upsert with array operations
await db.users.updateOne(
    { email: 'a@x.com' },
    {
        $setOnInsert: { createdAt: new Date() },
        $addToSet:    { tags: 'newsletter' },
    },
    { upsert: true },
);

// 11) Time-windowed counter (rate limit, daily metrics)
const today = new Date().toISOString().slice(0, 10);    // '2026-06-08'
await db.metrics.updateOne(
    { date: today, type: 'pageview' },
    {
        $inc:         { count: 1 },
        $setOnInsert: { date: today, type: 'pageview', createdAt: new Date() },
    },
    { upsert: true },
);

// 12) Cache-style upsert with TTL
await db.cache.updateOne(
    { key: 'user:42:profile' },
    {
        $set: { value: JSON.stringify(profile), expiresAt: new Date(Date.now() + 300_000) },
    },
    { upsert: true },
);

// Then a TTL index automatically removes expired entries
await db.cache.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });

// 13) Upsert + custom _id
await db.events.updateOne(
    { _id: req.headers['idempotency-key'] },   // client-supplied key
    { $setOnInsert: { payload: req.body, createdAt: new Date() } },
    { upsert: true },
);

// 14) When upserts are wrong
//   • You need to know if it was insert or update → use upsertedId in result
//   • You need different fields per branch → use findOneAndUpdate + check result
//   • Workflow requires a CREATE event vs UPDATE event → split into two operations
//   • You need transactional consistency across docs → use a session + transaction

// 15) Common bugs
//   • Forgetting unique index → duplicate documents under contention
//   • Using $set instead of $setOnInsert for fields that should only set once
//   • Forgetting findOneAndUpdate's returnDocument: 'after' → returns stale value
//   • Race condition without retry on duplicate-key error
//   • Upsert on filter that doesn't match → unexpected new docs

// 16) Performance
//   • Index the FILTER field — without an index, upsert scans the collection
//   • bulk upserts >>> sequential upserts (1 round trip vs N)
//   • For high-write workloads, sharded counters across multiple docs reduce contention

// 17) Best practices
//   • Pair upserts with a unique index on the filter field
//   • Use $setOnInsert for createdAt + generated IDs
//   • Use idempotency keys for HTTP request deduplication
//   • Catch + retry duplicate-key errors on concurrent upserts
//   • findOneAndUpdate when you need the resulting doc atomically

Why it matters

Upsert + unique index is the race-safe insert-or-update pattern. \$setOnInsert for “set once” fields (createdAt, generated IDs); \$set for everything you want to refresh on every call.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
db.counters.updateOne(
    { name: 'hits' },
    { $inc: { value: 1 } },
    { upsert: true }
);
Try it Yourself »

Discussion

Loading…