This tutorial was written by James Shisiah.
MongoDB is a document-oriented NoSQL database with a flexible schema designed to store, query, and manipulate large volumes of data. For PHP development, we have the official MongoDB PHP driver, which consists of a ) that exposes a clean API for interacting with collections, documents, and advanced database features, including full-text and semantic search on MongoDB Atlas.
With the library, you get tools for standard database operations like inserts, updates, and deletes. Additionally, it supports bulk write operations via the (a modern PHP installer for extensions)—used to install the for managing PHP dependencies, used to install level, you can perform bulk write operations on a single collection. The old MongoDB bulk write implementation provides two related APIs:
- MongoDB\Driver\Manager::executeBulkWrite()
- MongoDB\Collection::bulkWrite()
Collection::bulkWrite() is a convenience wrapper provided by the high-level PHP library. Internally, it prepares a MongoDB\Driver\BulkWrite object and executes it via Manager::executeBulkWrite(), automatically supplying the correct namespace. In other words, Collection::bulkWrite() object provides a namespace plus write operations to Manager::executeBulkWrite(), making your code more readable.
The Collection::bulkwrite method makes a database call for each type of write operation. It can perform multiple insert operations in one call, but it makes two separate calls to the database for an insert operation and a replace operation. While this is efficient when performing a bulk operation for one type of write operation—e.g., inserting thousands of rows from a CSV file—if you combine inserts and updates in one ops array, it will make separate calls to the database for each type (insert and update). Let’s explore sample code snippets with the old bulk write APIs.
Sample code with Manager::executeBulkWrite()
$bulk = new MongoDB\Driver\BulkWrite;
$bulk->insert(['name' => 'Alice']);
$bulk->update(['email' => '[email protected]'], ['$set' => ['active' => true]]);
$bulk->delete(['status' => 'inactive']);
$manager = new MongoDB\Driver\Manager($mongoDBuri);
$result = $manager->executeBulkWrite('bulkwritedb.users', $bulk);
Sample code with Collection::bulkwrite()
$client = new MongoDB\Client($mongoDBuri);
$database = $client->selectDatabase('bulkwritedb');
$collection = $database->selectCollection('users');
$operations = [
['insertOne' => [['name' => 'Alice', 'age' => 25]]],
['insertOne' => [['name' => 'Bob', 'age' => 30]]],
['insertOne' => [['name' => 'Charlie', 'age' => 28]]],
];
$result = $collection->bulkWrite($operations);
In this example, Collection::bulkWrite() internally provides the namespace bulkwritedb.users to Manager::executeBulkWrite(), which then makes the database call.
Keep in mind the following points when working with the old bulk write APIs:
- Operates on a single namespace
- Returns a single write result object
- Can fail if the server’s response exceeds the maximum BSON document size
- Well-suited for small to medium batch operations
New BulkWrite API (cluster/client level)
The new MongoDB BulkWrite API makes calls at the than bulkWrite operations on MongoDB 7.0. The new API performs all write operations (inserts, updates, replace, and delete) in one database call, unlike the old API, which makes different calls for each type of write operation. It follows the following execution path:
- Creates a
MongoDB\Driver\BulkWriteCommandinstance - Executes it via
Manager::executeBulkWriteCommand()
- Returns results using a cursor (the old API does not return results using a cursor)
By returning results through a cursor, MongoDB can stream responses incrementally rather than sending a single, potentially large BSON document. This allows us to work around the limitations of the old BulkWrite API and hence can handle much larger bulk operations safely. Your application will have better resilience during large imports and migrations.
Sample code with MongoDB\Client::bulkWrite()
$client = new MongoDB\Client($mongoDBuri);
// Create the ClientBulkWrite for your 'users' collection
$usersCollection = $client->bulkwritedb->users;
$bulkWrite = MongoDB\ClientBulkWrite::createWithCollection($usersCollection);
// Add your operations
$bulkWrite->insertOne(['name' => 'Alice', 'age' => 25]);
$bulkWrite->insertOne(['name' => 'Bob', 'age' => 30]);
$bulkWrite->insertOne(['name' => 'Charlie', 'age' => 28]);
// Perform the bulk write operation
$result = $client->bulkWrite($bulkWrite);
The sample code above demonstrates how to create a ClientBulkWrite instance from a MongoDB\Collection instance by using the createWithCollection() method.
In this guide, we will be working with the new MongoDB bulkWrite API. You will learn how to write to multiple collections by chaining calls to the createWithCollection() method on your MongoDB\Client::bulkWrite() instance. For modern, scalable applications, especially writing to multiple collections, we recommend using MongoDB\Client::bulkWrite() since it allows you to iterate through its results reliably.
1. Project setup
Before we start working with MongoDB’s BulkWrite operations in PHP, let’s set up a sample project environment. We have avoided working with frameworks so you can focus purely on understanding how MongoDB’s PHP extension and the high-level library work. The code for this project can be retrieved from this or the PHP manual. For a quick setup, you can install and enable it using . Install it with the command:
composer require mongodb/mongodb
Since we will be working with a MongoDB connection string, and we do not want it pushed to the git repositories, we will save it in an environment variable (.env) file. This file is then added to .gitignore so it’s ignored when pushing your code. Any dotenv library (symfony/dotenv, vlucas/phpdotenv, etc.) can be used to read environment variables. For this guide, we will be using the cluster, since it works online, requires no local installation, and gives you a clean connection URI.
However, if you prefer running , you can also use a local URI; the steps are the same. We have included a docker-compose.yml file that you can easily use to run the MongoDB Atlas Docker image locally.
Get your connection string
Visit docs outline clear steps you can follow to create a free cluster, set up user credentials, and add allowed IP addresses (here, you should include your current IP or add 0.0.0.0/0 to allow access from anywhere, for development purposes only).
We named our cluster BulkWrite for the sake of this tutorial. In the next step of the connect flow, under Choose a connection method, a connection string is displayed. You can choose your programming language (PHP) to view the code sample:
If, for some reason, you did not see the success message or there was an exception thrown in your code, consider checking out the w: 1 or higher (including majority). With w: 0, MongoDB does not wait for confirmation; the above result summary counts methods will return NULL. Default majority ensures the write is durable (can survive primary failover) across replica set failovers.
Sample code showing how to get summary counts
$result = $client->bulkWrite($bulkWrite);
echo "=== Summary Counts ===\n";
echo "Inserted: " . $result->getInsertedCount() . "\n";
echo "Matched: " . $result->getMatchedCount() . "\n";
echo "Modified: " . $result->getModifiedCount() . "\n";
echo "Upserted: " . $result->getUpsertedCount() . "\n";
echo "Deleted: " . $result->getDeletedCount() . "\n";
echo "Acknowledged: " . ($result->isAcknowledged() ? 'Yes' : 'No') . "\n";
Cursor-based verbose results (new API advantage)
The new BulkWrite API supports returning results in batches using a cursor rather than only summary counts, so very large responses don’t hit BSON size limits. To receive detailed, per-operation results from the new BulkWrite API, you should set the verboseResults option to true when calling bulkWrite. You can enable verboseResults in the $options array parameter in the following ways.
- When creating the ClientBulkWrite instance:
$bulkWrite = ClientBulkWrite::createWithCollection($collection, [
'ordered' => true,
'verboseResults' => true // Enable detailed per-operation results
]);
- When calling the bulkWrite command with your bulk write operations on your ClientBulkWrite instance:
$result = $client->bulkWrite(
$bulkWrite,
['verboseResults' => true]
);
With verboseResults enabled, the $result object will now expose $cursor-based results in the following methods:
- getInsertResults(): returns an array of detailed results for each insert operation (including insertedId)
- getUpdateResults(): returns an array of detailed results for each update (including matchedCount, modifiedCount, upsertedId)
- getDeleteResults(): returns an array of detailed results for each delete (including deletedCount)
Sample code showing how to loop and get the detailed results for each operation in your bulk write
$result = $client->bulkWrite($bulkWrite);
$insertResults = $result->getInsertResults();
if ($insertResults) {
foreach ($insertResults as $index => $insertResult) {
echo "Insert operation #$index:\n";
echo " Inserted ID: " . $insertResult->insertedId . "\n";
}
}
$updateResults = $result->getUpdateResults();
if ($updateResults) {
foreach ($updateResults as $index => $updateResult) {
echo "Update operation #$index:\n";
echo " Matched: " . $updateResult->matchedCount . "\n";
echo " Modified: " . $updateResult->modifiedCount . "\n";
if (isset($updateResult->upsertedId)) {
echo " Upserted ID: " . $updateResult->upsertedId . "\n";
}
}
}
$deleteResults = $result->getDeleteResults();
if ($deleteResults) {
foreach ($deleteResults as $index => $deleteResult) {
echo "Delete operation #$index:\n";
echo " Deleted: " . $deleteResult->deletedCount . "\n";
}
}
Understanding WriteConcerns
. The CSV files are included in the repository.
The customers’ CSV has the following columns:
Index,Customer Id,First Name,Last Name,Company,City,Country,Phone 1,Phone 2,Email,Subscription Date,Website
The organizations’ CSV has the following columns:
Index,Organization Id,Name,Website,Country,Description,Founded,Industry,Number of employees
CSV row → MongoDB document mapping
A CSV row like…
Why BulkWrite is ideal for CSV imports
- Faster imports: Instead of sending 10,000 inserts one by one, you only send two batches of 5000.
- Fewer network calls: Each bulk write = one network round-trip.
- Order is preserved: Documents are inserted in the order you read them, unless you use the unordered flow.
- Consistent error handling: You can log failures per batch without stopping the entire import.
- Works well with validation rules: You still get detailed error messages from BulkWriteCommandException.
6. Best practices when working with MongoDB BulkWrite
To get optimum performance, reliability, and clarity from your bulk operations, follow these recommended best practices. These guidelines are commonly used in production systems that handle imports, data migrations, and large update operations.
1. Use unordered writes when order does not matter
MongoDB supports two bulk modes:
a. Ordered (default)
- Operations run in sequence.
- If one operation fails, the rest stop.
- Ideal when order is important (e.g., dependent updates).
b. Unordered
- MongoDB executes operations in parallel, in any order.
- Failures do not stop the entire batch.
- Much faster for large writes.
For example, we use the unordered writes in the CSV import code sample.
2. Verbose results
In the CSV import code, verboseResults is disabled. You can enable it if you are experiencing errors in your import and want to perform some debugging/auditing. Otherwise, for memory efficiency, keep it disabled for large imports.
3. Manage large batches (chunking strategy)
While BulkWrite can theoretically process thousands of operations, large batches can:
- Increase memory usage.
- Risk timeouts.
- Make error reporting harder.
Chunking helps you:
- Control memory usage.
- Handle partial successes.
- Ensure predictable performance.
- Build resumable import systems—in case of a failure, you can resume from the batch number that failed, skipping others.
Example $batchSize = 5000;
4. Log errors for debugging failed operations
BulkWrite exceptions provide detailed information about:
- Duplicate key errors.
- Validation failures.
- Network issues.
- WriteConcern failures.
- Operations that failed.
You should always wrap BulkWrite in a try...catch block and log failure details as seen in the example code blocks above, because:
- BulkWrite may succeed partially, and you need to know where it failed.
- Logging helps diagnose invalid data, duplicate keys, or corrupted CSV entries.
- In production pipelines, logs help users retry or fix only the failed items.
5. Working with transactions for atomicity
When you need atomic guarantees, where either all operations succeed, or none are applied, then wrap your bulk write in a transaction. The require a replica set or sharded cluster; they are not available on standalone MongoDB deployments. For bulk writes, if a single operation cannot be successfully executed, any operations from the same bulk write that had already completed will only be rolled back if you used a transaction.
6. Additional best practices
Optimize indexing: When performing bulk updates or deletes, an indexed filter greatly speeds up execution. Additionally, avoid over-indexing. While indexes improve query speed, they can hinder write operations and consume additional disk space. Regularly review and remove unused or unnecessary indexes.
Validate data before adding to the batch: Catch “bad rows” early during CSV import rather than letting MongoDB reject them.
Use upserts carefully: They help merge data, but ensure your match filter is accurate.
Avoid mixing too many different operation types: Multiple inserts + updates + deletes are fine, but mixing extremely complex update operations may complicate debugging.
Monitor your MongoDB logs: MongoDB will log slow bulk operations or write concern issues.
Conclusion
BulkWrite is one of the most powerful additions to the MongoDB PHP library features. It proves useful when your application requires high-volume inserts, updates, and deletes. By combining multiple operations, you significantly reduce network round-trips and improve performance, especially when importing CSV data, huge system logs, cleaning up large datasets, or syncing records.
In this tutorial, we explored:
- How to connect to MongoDB (Atlas or local).
- How to perform bulk inserts, updates, and deletes.
- How to combine multiple operations in a single batch.
- How to handle results and errors.
- How to build a realistic CSV importer using BulkWrite.
- Best practices for performance and reliability.
With this knowledge, you’re ready to build fast, scalable data-processing scripts in PHP using MongoDB.
Next steps
Here are helpful resources to deepen your understanding:
MongoDB error handling/WriteConcern docs
SOCIAL SHARE CARD GENERATOR