updateBatch - Update documents in batches

API parameter description

Each selected document is checked against the original business filter again when its write is dispatched. Documents that no longer match are skipped. On a fatal batch error, no new batch is acquired; already running batches finish before the error is returned. For strict Model updates, conflictCount refers to a version mismatch, while a changed filter or concurrent deletion is counted as skipped.

Method signature

collection(name: string).updateBatch(
  filter: object,
  update: object,
  options?: UpdateBatchOptions
): Promise<UpdateBatchResult>

Detailed explanation of parameters

First parameter: filter (required)

  • Type: object
  • Description: Update conditions, same as updateMany

Second parameter: update (required)

  • Type: object
  • Description: For update operations, must use update operator ($set, $inc, $push, etc.)
  • ❌ Error: { name: 'new' }
  • ✅ Correct: { $set: { name: 'new' } }

Third parameter: options (optional)

ParametersTypeDefault valueDescription
batchSizenumber1000Number of documents updated in each batch
estimateProgressbooleantrueWhether to pre-count the total number (for progress percentage)
onProgressFunction-Progress callback function (progress) => {}
onErrorstring'stop'Error handling strategy: 'stop'/'skip'/'collect'/'retry'
retryAttemptsnumber3The maximum number of retries for a failed batch (when onError='retry')
retryDelaynumber1000Retry delay time (milliseconds)
onRetryFunction-Retry callback function (retryInfo) => {}
writeConcernobject{ w: 1 }Write confirmation level
upsertbooleanfalseUnsupported for updateBatch; passing true throws. Use upsertOne for single-document upserts, or updateMany(..., { upsert: true }) only when you want MongoDB's native single-insert-on-no-match semantics.
arrayFiltersArray-Array filters
commentstring-Operation comments (for log tracking)

Return value

{
  acknowledged: boolean,      //Is it confirmed
  totalCount: number | null,  //Total number of documents (valid when estimateProgress=true)
  matchedCount: number,       //Number of matching documents
  modifiedCount: number,      //Number of successful updates
  upsertedCount: number,      //Always 0 because updateBatch rejects upsert=true
  batchCount: number,         //Total number of batches
  errors: Array<Object>,      //error list
  retries: Array<Object>      //Retry record list
}

Progress callback parameters

{
  currentBatch: number,    //Current batch number (starting from 1)
  totalBatches: number,    //Total number of batches
  modified: number,        //Quantity updated
  total: number | null,    //Total quantity (valid when estimateProgress=true)
  percentage: number | null, //Complete percentage (0-100, has value when estimateProgress=true)
  errors: number,          //number of errors
  retries: number          //Number of retries
}

Usage example

1. Basic usage - batch modification status

//Mark all pending orders as processed
const result = await collection('orders').updateBatch(
    { status: 'pending' },
    { $set: { status: 'processed', processedAt: new Date() } },
    { batchSize: 5000 }
);

console.log(`Update ${result.modifiedCount} orders`);

2. With progress monitoring - data migration

const result = await collection('users').updateBatch(
    { oldField: { $exists: true } },
    {
        $set: { newField: 'value' },
        $unset: { oldField: '' }
    },
    {
        batchSize: 5000,
        estimateProgress: true,
        onProgress: (progress) => {
            console.log(`Migration progress: ${progress.percentage}% (${progress.modified}/${progress.total} items)`);
        }
    }
);

Example output:

Migration progress: 20% (100000/500000 items)
Migration progress: 40% (200000/500000 items)
Migration progress: 60% (300000/500000 items)
Migration progress: 80% (400000/500000 items)
Migration progress: 100% (500000/500000 items)

3. $set - Set field value

//Set user membership levels in batches
await collection('users').updateBatch(
    { registeredDays: { $gte: 365 } },
    {
        $set: {
            vipLevel: 'gold',
            vipStartAt: new Date()
        }
    },
    { batchSize: 5000 }
);

4. $inc - increase or decrease the value

//Increase product inventory in batches
await collection('products').updateBatch(
    { category: 'electronics' },
    {
        $inc: {
            stock: 100,        //Inventory +100
            version: 1         //version number +1
        }
    },
    { batchSize: 3000 }
);

5. $push - add array elements

//Add tags to users in batches
await collection('users').updateBatch(
    { isActive: true },
    {
        $push: {
            tags: 'promoted',
            notifications: {
                type: 'promo',
                createdAt: new Date()
            }
        }
    },
    { batchSize: 5000 }
);

6. $pull - delete array elements

//Delete expired notifications in batches
await collection('users').updateBatch(
    { 'notifications.0': { $exists: true } },
    {
        $pull: {
            notifications: {
                expiresAt: { $lt: new Date() }
            }
        }
    },
    { batchSize: 5000 }
);

7. $mul - multiplication operation

//Bulk price adjustment (all prices increased by 10%)
await collection('products').updateBatch(
    { price: { $gt: 0 } },
    {
        $mul: { price: 1.1 },
        $set: { updatedAt: new Date() }
    },
    {
        batchSize: 5000,
        estimateProgress: true,
        onProgress: (p) => console.log(`Price adjustment progress: ${p.percentage}%`)
    }
);

8. Multiple operator combinations

//Complex batch update: price adjustment + inventory increase + label addition
await collection('products').updateBatch(
    { category: 'sale' },
    {
        $mul: { price: 0.8 },              //20% off price
        $inc: { stock: 50 },               //Inventory +50
        $push: { tags: 'discount' },       //Add discount tag
        $set: {
            onSale: true,
            saleStartAt: new Date()
        }
    },
    { batchSize: 3000 }
);

9. Using arrayFilters - Update specific elements in an array

//Batch update the status of specific items in an order
await collection('orders').updateBatch(
    { 'items.status': 'pending' },
    {
        $set: {
            'items.$[elem].status': 'shipped',
            'items.$[elem].shippedAt': new Date()
        }
    },
    {
        batchSize: 3000,
        arrayFilters: [{ 'elem.status': 'pending' }]
    }
);
const result = await collection('users').updateBatch(
    { lastActive: { $lt: new Date('2024-01-01') } },
    { $set: { status: 'inactive' } },
    {
        batchSize: 5000,
        onError: 'retry',
        retryAttempts: 3,
        retryDelay: 1000,
        onRetry: (info) => {
            console.log(`Batch ${info.batchIndex + 1} retry ${info.attempt}`);
        }
    }
);

console.log(`Update completed, retry ${result.retries.length} times`);

11. Upsert is not supported

updateBatch first selects matching document _id values through a cursor and then updates those _id batches. That batching model has no stable "insert when none matched" target, so upsert: true is rejected.

Use upsertOne() for single-document upserts, or MongoDB-native updateMany(..., { upsert: true }) when you explicitly want server-side multi-update semantics.

MongoDB native note: updateMany(filter, update, { upsert: true }) is supported, but if no documents match, MongoDB inserts one new document derived from the equality parts of filter and the update document. It is not a per-input bulk upsert. For multiple independent keys, run separate upsertOne() calls or use native bulkWrite updateOne models with upsert: true.

//Correct: single-document upsert
await collection('user_settings').upsertOne(
    { userId: 'user_123' },
    {
        $set: {
            theme: 'light',
            language: 'en',
            updatedAt: new Date()
        },
        $setOnInsert: {
            createdAt: new Date()
        }
    }
);

12. Complex query conditions

//Batch update documents that meet multiple conditions
await collection('orders').updateBatch(
    {
        status: 'pending',
        createdAt: { $lt: new Date('2024-01-01') },
        $or: [
            { paymentStatus: 'paid' },
            { amount: 0 }
        ]
    },
    {
        $set: {
            status: 'cancelled',
            cancelledAt: new Date(),
            cancelReason: 'Automatically cancel after timeout'
        }
    },
    {
        batchSize: 5000,
        estimateProgress: true,
        onProgress: (p) => console.log(`Cancellation progress: ${p.percentage}%`)
    }
);

Performance optimization suggestions

1. Batch size selection

Data volumeRecommended batchSizeReason
< 100,0001000-2000Small batch, quick response
100,000-1 million3000-5000Balance performance and memory
> 1 million5000-10000Large batches, reducing network overhead
//Dynamically adjust batch size
const totalCount = await collection('users').count({ status: 'inactive' });
const batchSize = totalCount > 1000000 ? 10000 : 5000;

await collection('users').updateBatch(
    { status: 'inactive' },
    { $set: { status: 'archived' } },
    { batchSize }
);

2. Index optimization

//Make sure there is an index before updating
await collection('orders').createIndex({ status: 1, createdAt: 1 });

//and then update
await collection('orders').updateBatch(
    { status: 'pending', createdAt: { $lt: expireDate } },
    { $set: { status: 'expired' } },
    { batchSize: 5000 }
);

3. Avoid full table scan

//❌ Bad: Unindexed fields
await collection('users').updateBatch(
    { customField: 'value' },  //If customField does not have an index, the entire table will be scanned.
    { $set: { status: 'updated' } }
);

//✅ Good: Use indexed fields
await collection('users').updateBatch(
    { _id: { $in: userIds } },  //_id has a default index
    { $set: { status: 'updated' } }
);

4. Update operation optimization

//❌ Inefficiency: updating the same batch of data multiple times
await collection('users').updateBatch(filter, { $set: { field1: 'a' } });
await collection('users').updateBatch(filter, { $set: { field2: 'b' } });

//✅ Efficient: update multiple fields at once
await collection('users').updateBatch(filter, {
    $set: {
        field1: 'a',
        field2: 'b'
    }
});

FAQ

Q1: What is the difference between updateBatch and updateMany?

Compare itemsupdateBatchupdateMany
Applicable data volume> 10000 items< 10000 items
Memory usageConstant (streaming)Linear growth
Progress Monitoring✅ Supported❌ Not Supported
Error Handling✅ 4 Strategies❌ Only Fail All
Auto Retry✅ Supported❌ Not Supported
PerformanceBetter for large data volumesFaster for small data volumes

Suggestions:

  • Data volume < 10000 items → use updateMany
  • Data volume ≥ 10000 → use updateBatch

Q2: Will updateBatch cause data inconsistency?

Answer: updateBatch processes matching _id values through a cursor and writes them in batches. It does not create a MongoDB transaction or guarantee snapshot isolation by itself. If you need a transactional snapshot, run it inside an explicit transaction and pass the transaction session.

//Use a transaction when the batch must share the same transactional snapshot.
await collection('users').updateBatch(
    { status: 'inactive' },
    { $set: { status: 'archived' } },
    { batchSize: 5000 }
);

Q3: Why must we use the update operator?

Answer: This is a MongoDB requirement, as is updateMany.

//❌ Error: direct assignment
await collection('users').updateBatch(
    { status: 'old' },
    { status: 'new' }  //will throw an error
);

//✅ Correct: use $set
await collection('users').updateBatch(
    { status: 'old' },
    { $set: { status: 'new' } }
);

Q4: How to update _id in batches?

Answer: Updating _id is not recommended. If necessary, use replaceOne or reinsert.

//❌ Not supported: update _id
await collection('users').updateBatch(
    { oldId: { $exists: true } },
    { $set: { _id: newId } }  //will fail
);

//✅ Recommendation: Keep the old ID and add new fields
await collection('users').updateBatch(
    { oldId: { $exists: true } },
    {
        $set: { newId: generateNewId() },
        $unset: { oldId: '' }
    }
);

Q5: What update operators does updateBatch support?

A: All MongoDB update operators are supported.

Field operators:

  • $set - Set field value
  • $unset - Delete field
  • $rename - Rename fields
  • $setOnInsert - Accepted by MongoDB update documents, but only meaningful in upsert operations; updateBatch rejects upsert: true

Numeric operators:

  • $inc - increase or decrease
  • $mul - Multiplication
  • $min - take the minimum value
  • $max - take the maximum value

Array Operator:

  • $push - Add element
  • $pop - delete the first/last elements
  • $pull - Remove matching elements
  • $pullAll - delete multiple elements
  • $addToSet - Add unique element

Other operators:

  • $currentDate - Set current date

Q6: How to estimate the update time?

//Performance Reference (In-Memory Database)
//Update speed: about 30000-40000 items/second

const totalCount = 1000000;
const estimatedTime = totalCount / 35000;  //about 29 seconds

console.log(`Estimated time: ${Math.ceil(estimatedTime)} seconds`);

Q7: Can it be used in transactions?

Answer: Yes.

const session = await msq.startSession();

try {
    await session.withTransaction(async () => {
        await collection('orders').updateBatch(
            { status: 'pending' },
            { $set: { status: 'processing' } },
            { batchSize: 1000 }
        );

        await collection('inventory').updateBatch(
            { productId: { $in: productIds } },
            { $inc: { reserved: -1 } },
            { batchSize: 1000 }
        );
    });
} finally {
    await session.endSession();
}

References