Senior 13 min · March 06, 2026

PHP MongoDB — Silent Data Loss from Default Write Concern

MongoDB default {w:1} write concern causes silent data loss after replica set elections.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • PHP + MongoDB pairs PHP's dynamic typing with MongoDB's schemaless documents
  • The mongodb/mongodb library wraps ext-mongodb C extension for near-native speed
  • BSON serialisation handles type conversion between PHP types and MongoDB types
  • A single document can hold up to 16MB, but keep it under 1MB for cursor performance
  • Wrong index choice adds 200ms+ per query in production — use explain() before trusting
  • Biggest mistake: ignoring write concern leads to silent data loss in replica sets
✦ Definition~90s read
What is PHP and MongoDB?

At its heart, PHP+MongoDB means using PHP to interact with a document-oriented NoSQL database. You store data as BSON documents — think JSON but with strongly-typed fields. The PHP driver handles the serialisation and deserialisation so you work with native PHP arrays or objects.

Imagine a traditional MySQL database is like a perfectly organised filing cabinet where every folder must follow the exact same template — same slots, same fields, no exceptions.

This pairing shines when your data model changes frequently, when you need to embed related data directly, or when you need high write throughput. For example, a user profile with address history and preferences can be stored in a single document instead of normalising across three tables.

That's not just convenience — it eliminates expensive joins and makes read operations 10x faster in many cases.

Here's how a basic query looks in PHP:

```php <?php namespace Io\TheCodeForge\MongoDb;

use MongoDB\Client;

$client = new Client(getenv('MONGODB_URI')); $collection = $client->selectDatabase('ecommerce')->selectCollection('products');

$product = $collection->findOne(['sku' => 'PHONE-001']); echo $product['name'] . ' - $' . $product['price']; ```

Notice you're working with arrays directly — no schema definition, no migration, no ORM configuration. That's the speed advantage that matters when your data shape changes weekly.

Plain-English First

Imagine a traditional MySQL database is like a perfectly organised filing cabinet where every folder must follow the exact same template — same slots, same fields, no exceptions. MongoDB is like a collection of labeled shoeboxes where each box can hold whatever you want — photos, receipts, a rubber duck — and you can find anything fast because each box has a smart index on the outside. PHP is the person opening those boxes, reading what's inside, and deciding what to do next. When your data is irregular, fast-changing, or shaped differently for every user, the shoebox system wins hands down.

Most PHP applications start life with MySQL — structured, reliable, familiar. But somewhere around the time your product manager asks for 'flexible user profiles', 'nested product attributes', or 'activity feeds that look different for every user type', a relational schema starts to feel like wearing shoes two sizes too small. You spend more time writing ALTER TABLE migrations and LEFT JOIN acrobatics than you spend actually building features. That's not a MySQL problem — it's a data-shape mismatch problem, and MongoDB was built to solve it at scale.

MongoDB stores data as BSON documents — Binary JSON objects that can nest arrays, sub-documents, and mixed types without a schema police officer stopping you at the door. PHP connects to it via the official mongodb/mongodb Composer package, which wraps the low-level ext-mongodb C extension. This split architecture (C extension for raw performance, PHP library for ergonomic developer experience) means you get near-native speed without writing a single line of C. The driver handles connection pooling, BSON serialisation, cursor streaming, and write concern negotiation transparently.

By the end of this article you'll know how to wire up PHP to MongoDB correctly in production, write efficient CRUD operations and aggregation pipelines, design indexes that don't destroy your write throughput, and dodge the six most painful production mistakes that nobody warns you about until your on-call phone rings at 2am.

What is PHP and MongoDB?

At its heart, PHP+MongoDB means using PHP to interact with a document-oriented NoSQL database. You store data as BSON documents — think JSON but with strongly-typed fields. The PHP driver handles the serialisation and deserialisation so you work with native PHP arrays or objects. This pairing shines when your data model changes frequently, when you need to embed related data directly, or when you need high write throughput. For example, a user profile with address history and preferences can be stored in a single document instead of normalising across three tables. That's not just convenience — it eliminates expensive joins and makes read operations 10x faster in many cases.

```php <?php namespace Io\TheCodeForge\MongoDb;

use MongoDB\Client;

$client = new Client(getenv('MONGODB_URI')); $collection = $client->selectDatabase('ecommerce')->selectCollection('products');

$product = $collection->findOne(['sku' => 'PHONE-001']); echo $product['name'] . ' - $' . $product['price']; ```

Notice you're working with arrays directly — no schema definition, no migration, no ORM configuration. That's the speed advantage that matters when your data shape changes weekly.

FirstQuery.phpPHP
1
2
3
4
5
6
7
8
9
10
<?php
namespace Io\TheCodeForge\MongoDb;

use MongoDB\Client;

$client = new Client(getenv('MONGODB_URI'));
$collection = $client->selectDatabase('ecommerce')->selectCollection('products');

$product = $collection->findOne(['sku' => 'PHONE-001']);
echo $product['name'] . ' - $' . $product['price'];
Output
iPhone 16 - $999
Forge Tip:
Type this code yourself rather than copy-pasting. The muscle memory of writing it will help it stick.
Production Insight
The split architecture (C extension + PHP library) means you must install both.
Missing the C extension silently falls back to a slower pure-PHP implementation, adding ~20ms per request.
Rule: always verify ext-mongodb is loaded with php -m | grep mongodb before deploying.
Don't assume the Composer package implies the C extension is present.
Key Takeaway
Two parts needed: ext-mongodb (C) and mongodb/mongodb (PHP library).
Verify extension loaded.
Slow fallback if missing.
PHP MongoDB Write Concern Data Loss Risk THECODEFORGE.IO PHP MongoDB Write Concern Data Loss Risk Flow from default write concern to silent failure in replica sets PHP MongoDB Driver Default w=1 write concern Primary Acknowledges Write w=1 waits for primary only Primary Crashes Before Replication Secondary not yet updated Failover to Stale Secondary New primary lacks the write Write Lost Silently No error returned to app Use w=majority Wait for replica set majority ⚠ Default w=1 can lose acknowledged writes on failover Always set w=majority and j=true for critical data THECODEFORGE.IO
thecodeforge.io
PHP MongoDB Write Concern Data Loss Risk
Php Mongodb

Driver Installation and Connection Setup

Getting PHP to talk to MongoDB involves two components: the C extension (ext-mongodb) and the PHP library (mongodb/mongodb). Install both via PECL and Composer:

``bash pecl install mongodb echo "extension=mongodb.so" >> php.ini composer require mongodb/mongodb ``

Then create a connection manager. Never hardcode credentials — use environment variables.

```php <?php namespace Io\TheCodeForge\MongoDb;

use MongoDB\Client; use MongoDB\Driver\Manager;

class ConnectionManager { private static ?Client $client = null;

public static function getClient(): Client { if (self::$client === null) { $uri = getenv('MONGODB_URI') ?: 'mongodb://localhost:27017'; $uriOptions = [ 'readPreference' => 'secondaryPreferred', 'w' => 'majority', 'journal' => true, 'connectTimeoutMS' => 3000, 'socketTimeoutMS' => 10000 ]; $driverOptions = [ 'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'], ]; self::$client = new Client($uri, $uriOptions, $driverOptions); } return self::$client; } } ```

The typeMap option converts BSON documents to PHP arrays instead of stdClass objects — faster and less error-prone. The readPreference lets read queries hit secondaries, offloading the primary.

One more thing: always set a reasonable connectTimeoutMS (like 3000ms) and socketTimeoutMS (like 10000ms) in your uriOptions. Without these, a MongoDB node that's slow to respond can hang your PHP process indefinitely. Trust me, you'll learn this the hard way when your FPM workers pile up waiting for a dead secondary.

ConnectionManager.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
namespace Io\TheCodeForge\MongoDb;

use MongoDB\Client;
use MongoDB\Driver\Manager;

class ConnectionManager
{
    private static ?Client $client = null;

    public static function getClient(): Client
    {
        if (self::$client === null) {
            $uri = getenv('MONGODB_URI') ?: 'mongodb://localhost:27017';
            $uriOptions = [
                'readPreference' => 'secondaryPreferred',
                'w' => 'majority',
                'journal' => true,
                'connectTimeoutMS' => 3000,
                'socketTimeoutMS' => 10000
            ];
            $driverOptions = [
                'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'],
            ];
            self::$client = new Client($uri, $uriOptions, $driverOptions);
        }
        return self::$client;
    }
}
Output
Returns a singleton MongoDB\Client instance with production-safe options.
Connection Pool Pitfall
The PHP driver handles connection pooling internally, but each process (Apache worker or FPM pool) creates its own pool. Under high concurrency, you can exhaust the default pool size (size=100). Increase it with maxPoolSize in uriOptions: 'maxPoolSize' => 200. Also monitor db.serverStatus().connections on MongoDB.
Production Insight
MongoDB's PHP driver uses per-process pools with a default size of 100 connections.
On busy FPM servers with 200 children, you'll hit connection exhaustion and get 'no suitable servers found' errors.
Rule: set maxPoolSize to child count + 50, and always monitor connection counts on the MongoDB side.
Another trap: forgetting to close cursors can keep connections open — always iterate or explicitly close.
Key Takeaway
Install ext-mongodb + mongodb/mongodb.
Singleton client with explicit readPreference, writeConcern, typeMap.
Set maxPoolSize to handle FPM concurrency.
Always set timeouts to avoid hung children.
Connection Decision: Single vs Multiple Clients
IfSingle database, single replica set
UseUse one singleton Client instance — reuse throughout the app
IfMultiple databases or clusters
UseCreate one Client per cluster, manage via factory
IfNeed different read preferences per query
UseSet default in Client, override per operation

CRUD Operations with Documents

MongoDB documents are BSON objects with nested structures. PHP's mongodb/mongodb library maps PHP arrays to BSON automatically. But watch out for type conversions — timestamps, ObjectIds, and large integers are common traps.

Insert: Pass an array with keys. Use ['_id' => new MongoDB\BSON\ObjectId()] only if you need client-generated IDs. Otherwise let MongoDB handle it.

Find: The find() method returns a MongoDB\Collection object that iterates lazily. Use ->toArray() for small result sets, cursor iteration for large ones. Always pass projection to limit fields returned over the wire.

Update: Use updateOne() or updateMany() with atomic operators like $set, $unset, $inc. Never read-modify-write — that's a race condition in disguise.

Delete: deleteOne() and deleteMany() are final. Use them sparingly; mark documents as deleted: true instead for recoverability.

```php <?php namespace Io\TheCodeForge\MongoDb;

use MongoDB\Collection; use MongoDB\Driver\Exception\RuntimeException;

class UserRepository { private Collection $collection;

public function __construct() { $this->collection = ConnectionManager::getClient() ->selectDatabase('myapp') ->selectCollection('users'); }

public function updateEmail(string $userId, string $newEmail): bool { try { $result = $this->collection->updateOne( ['_id' => new \MongoDB\BSON\ObjectId($userId)], ['$set' => ['email' => $newEmail]] ); return $result->getModifiedCount() === 1; } catch (RuntimeException $e) { error_log("MongoDB update failed: " . $e->getMessage()); return false; } }

public function batchUpdateStatus(array $userIds, string $status): int { $operations = []; foreach ($userIds as $id) { $operations[] = [ 'updateOne' => [ ['_id' => new \MongoDB\BSON\ObjectId($id)], ['$set' => ['status' => $status]] ] ]; } $result = $this->collection->bulkWrite($operations, ['ordered' => false]); return $result->getModifiedCount(); } } ```

bulkWrite with ordered => false runs operations in parallel — use it for batch jobs where order doesn't matter. For account transfers, keep ordered => true to maintain sequence.

One more thing: findOneAndUpdate is your atomic read-modify-write tool. It returns the updated document and guarantees no other process snuck in between. Perfect for counters, reservation systems, and queue pop operations.

UserRepository.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
namespace Io\TheCodeForge\MongoDb;

use MongoDB\Collection;
use MongoDB\Driver\Exception\RuntimeException;

class UserRepository
{
    private Collection $collection;

    public function __construct()
    {
        $this->collection = ConnectionManager::getClient()
            ->selectDatabase('myapp')
            ->selectCollection('users');
    }

    public function updateEmail(string $userId, string $newEmail): bool
    {
        try {
            $result = $this->collection->updateOne(
                ['_id' => new \MongoDB\BSON\ObjectId($userId)],
                ['$set' => ['email' => $newEmail]]
            );
            return $result->getModifiedCount() === 1;
        } catch (RuntimeException $e) {
            error_log("MongoDB update failed: " . $e->getMessage());
            return false;
        }
    }

    public function batchUpdateStatus(array $userIds, string $status): int
    {
        $operations = [];
        foreach ($userIds as $id) {
            $operations[] = [
                'updateOne' => [
                    ['_id' => new \MongoDB\BSON\ObjectId($id)],
                    ['$set' => ['status' => $status]]
                ]
            ];
        }
        $result = $this->collection->bulkWrite($operations, ['ordered' => false]);
        return $result->getModifiedCount();
    }
}
Output
Updates a user's email atomically and batch-updates statuses.
Atomic Update Mental Model
  • updateOne() applies changes to the first matching document
  • updateMany() applies changes to all matching documents
  • Use $set to change specific fields, $unset to remove fields
  • Use $inc to safely increment/decrement counters
  • Never read-then-write — replace with update or findAndModify
Production Insight
Many PHP apps use find() followed by updateOne() based on condition — that's a race condition.
If two processes read the same document, both see the same state, then both write different updates, one overwrites the other.
Rule: use $inc for counters, $push for arrays, and findOneAndUpdate for read-modify-write patterns.
Even with atomic operators, check modified count — a matching document may not exist, and you'll get zero modifications without error.
Key Takeaway
Prefer atomic operators ($set, $inc, $push) over read-modify-write.
Always set writeConcern to majority on updates.
Use projection on find() to reduce network payload.
Update Strategy Decision Tree
IfSingle field update, no read needed
UseUse $set or $inc directly — atomic and fast
IfNeed to read current value then update
UseUse findOneAndUpdate with returnDocument => AFTER
IfUpdate multiple documents independently
UseUse bulkWrite with ordered => false

Aggregation Pipeline: Powerful Data Processing

MongoDB's aggregation pipeline processes documents through a sequence of stages: $match, $group, $sort, $project, $lookup, $unwind, etc. Each stage transforms the document stream, and the pipeline is executed in memory unless allowDiskUse is set.

Performance killers in production
  • $lookup without indexes on the foreign collection (adds seconds per query)
  • $unwind on large arrays followed by $group (creates massive intermediate results)
  • $sort without index at the start (forces all documents into memory)
  • $match after $group (filters reduced data, wastes early elimination)

Best practice: Place $match as early as possible. Use $project to drop unused fields. For expensive $lookup, consider denormalizing data if the foreign collection is mostly static.

``php $pipeline = [ ['$match' => ['created_at' => ['$gte' => new \MongoDB\BSON\UTCDateTime($startOfDay)]]], ['$group' => [ '_id' => '$status', 'count' => ['$sum' => 1], 'totalValue' => ['$sum' => '$total_amount'] ]], ['$sort' => ['totalValue' => -1]] ]; $orders = $collection->aggregate($pipeline, ['allowDiskUse' => true, 'maxTimeMS' => 5000]); foreach ($orders as $order) { echo "{$order['_id']}: {$order['count']} orders, total \${$order['totalValue']} "; } ``

Note: allowDiskUse is necessary for large datasets — otherwise MongoDB may throw a 16819 error when memory limit is exceeded.

A common trap with $lookup is forgetting that the localField value is taken as-is, so if it's a string, the foreign field must also be a string — not an ObjectId. This mismatch causes silent empty results and hours of debugging. Always check types in the explain output.

aggregate_orders.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace Io\TheCodeForge\MongoDb;

$collection = ConnectionManager::getClient()
    ->selectDatabase('ecommerce')
    ->selectCollection('orders');

$startOfDay = (new \DateTime('today midnight'))->getTimestamp() * 1000;

$pipeline = [
    ['$match' => ['created_at' => ['$gte' => new \MongoDB\BSON\UTCDateTime($startOfDay)]]],
    ['$group' => [
        '_id' => '$status',
        'count' => ['$sum' => 1],
        'totalValue' => ['$sum' => '$total_amount']
    ]],
    ['$sort' => ['totalValue' => -1]]
];
$orders = $collection->aggregate($pipeline, ['allowDiskUse' => true, 'maxTimeMS' => 5000]);
foreach ($orders as $order) {
    echo "{$order['_id']}: {$order['count']} orders, total \${$order['totalValue']}\n";
}
Output
pending: 15 orders, total $3200
shipped: 42 orders, total $8900
Aggregations on Sharded Collections
$lookup across sharded collections triggers scatter-gather — each shard executes the lookup independently, and results are merged by the primary shard. This can be extremely slow. If possible, use $lookup only on unsharded or small collections. Alternatively, duplicate data instead of joining.
Production Insight
A pipeline with $lookup on an unindexed foreign collection can take 10+ seconds for 10k source documents.
The fix: add an index on the foreign collection's join field.
Also, consider that $lookup does not use the local collection's shard key — it targets the foreign collection as a whole.
Rule: always create an index on {foreignKey: 1} in the foreign collection before deploying pipelines with $lookup.
Another silent killer: $lookup with 'let' and 'pipeline' syntax is more efficient but requires MongoDB 5.0+. If your cluster is older, fall back to the simpler localField/foreignField syntax.
Key Takeaway
Place $match first.
Index foreign keys for $lookup.
Set allowDiskUse and maxTimeMS on every pipeline.
Avoid $lookup on sharded collections if possible.
Aggregation Stage Order Decision
IfNeed to filter documents before grouping
UsePlace $match as first stage — reduces input size
IfNeed to join another collection
UseUse $lookup after $match, ensure foreign field indexed
IfLarge dataset, memory concerns
UseAdd allowDiskUse: true and set maxTimeMS

Indexing Strategy and Production Gotchas

Indexes in MongoDB are B-trees, similar to relational databases. But compound index prefix rules, query shape sensitivity, and the impact on write throughput differ.

Single field index: createIndex(['email' => 1]) — for exact matches or sort on email.

Compound index: createIndex(['status' => 1, 'created_at' => -1]) — supports queries on status, or status+created_at, but NOT on created_at alone.

Text index: createIndex(['description' => 'text']) — for full-text search with $text queries. Only one text index per collection.

Production pitfalls: 1. Too many indexes on a write-heavy collection -> each insert/update touches every index, doubling write time. 2. Unused indexes: use $indexStats to identify dead indexes. 3. Overusing hint() to force an index -> query optimizer disabled. 4. Not using dropIndexes() during migrations; building new indexes on large collections blocks read operations.

``php $cursor = $collection->find( ['status' => 'active', 'created_at' => ['$gte' => $date]], ['projection' => ['_id' => 0, 'email' => 1]] ); $explain = $collection->find( ['status' => 'active', 'created_at' => ['$gte' => $date]], ['projection' => ['_id' => 0, 'email' => 1], 'explain' => true] ); ``

The explain output shows whether a COLLSCAN (collection scan) or IXSCAN (index scan) is used. Aim for IXSCAN with low totalDocsExamined relative to nReturned.

One more gotcha: MongoDB's query planner caches plans. If a suboptimal plan gets cached because of a skewed data distribution, you'll see slow queries even with the right indexes. Run db.collection.getPlanCache().clear() occasionally to force re-evaluation.

create_indexes.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
namespace Io\TheCodeForge\MongoDb;

use MongoDB\Collection;

$ordersCollection = ConnectionManager::getClient()
    ->selectDatabase('ecommerce')
    ->selectCollection('orders');

// Compound index for common query pattern
$ordersCollection->createIndex(
    ['status' => 1, 'created_at' => -1, 'customer_id' => 1],
    ['background' => true]
);

// Text index for product search
$productsCollection->createIndex(
    ['description' => 'text', 'name' => 'text'],
    ['default_language' => 'english']
);

// Monitor index usage
$indexStats = $ordersCollection->aggregate([['$indexStats' => []]]);
foreach ($indexStats as $stat) {
    echo "Index: " . $stat['name'] . " - accesses: " . $stat['accesses']['ops'] . "\n";
}
Output
Index: status_1_created_at_-1_customer_id_1 - accesses: 1500
Index: _id_ - accesses: 200
Index Build Performance
Building an index on a collection with millions of documents can take minutes and degrade write performance. Use {background: true} (default in MongoDB 4.2+) to allow reads/writes during build. For replica sets, build on secondary first, then step it up to primary.
Production Insight
A team once created 12 indexes on a collection that received 10k writes/second.
Each write updated all 12 indexes, doubling latency and causing replication lag.
They dropped 6 unused indexes (detected via $indexStats) and moved 2 compound indexes to cover multiple query patterns.
Write latency dropped from 50ms to 12ms.
Rule: maximum 5 indexes per collection for write-heavy workloads; use compound indexes to cover multiple query patterns.
Also, avoid 'index intersection' — MongoDB can use multiple indexes per query but it's slow. Prefer compound indexes.
Key Takeaway
Use compound indexes to cover query patterns.
Explain() every query before production.
Monitor index usage with $indexStats.
Limit indexes on write-heavy collections.
Index Type Decision
IfQuery filters on one field only
UseSingle field index — simple, efficient
IfQuery filters on multiple fields in specific order
UseCompound index matching the query pattern
IfNeed full-text search on string fields
UseText index — one per collection
IfWrite throughput is critical
UseLimit indexes to max 5 per collection

Transactions, Write Concerns, and Data Integrity

MongoDB supports multi-document ACID transactions since version 4.0. But they come with caveats: they only work on replica sets, have a 60-second default timeout, and should not be used for high-throughput operations that can be done with atomic operators.

Write concern controls durability. The default w: 1 acknowledges after primary memory write — not durable. w: majority waits for majority of replica set members. Adding j: true forces journal commit before acknowledgment.

Read concerns: local (default) returns data that may be rolled back. majority returns only committed data. Use linearizable only when you need absolute latest data, but it's slow.

When to use transactions
  • Multiple documents that must be updated together (e.g., funds transfer)
  • Cross-collection consistency
  • Not for simple counters or array pushes — those are atomic anyway.

```php <?php namespace Io\TheCodeForge\MongoDb;

$client = ConnectionManager::getClient(); $maxRetries = 3; $retry = 0;

do { $session = $client->startSession(); $session->startTransaction([ 'readConcern' => new \MongoDB\Driver\ReadConcern(\MongoDB\Driver\ReadConcern::SNAPSHOT), 'writeConcern' => new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY) ]); try { $accounts = $client->selectDatabase('bank')->selectCollection('accounts'); $accounts->updateOne( ['_id' => new \MongoDB\BSON\ObjectId($fromId)], ['$inc' => ['balance' => -100]], ['session' => $session] ); $accounts->updateOne( ['_id' => new \MongoDB\BSON\ObjectId($toId)], ['$inc' => ['balance' => 100]], ['session' => $session] ); $session->commitTransaction(); break; } catch (\MongoDB\Driver\Exception\CommandException $e) { $session->abortTransaction(); if ($e->hasErrorLabel('TransientTransactionError') && ++$retry < $maxRetries) { usleep(100000); // 100ms backoff continue; } throw $e; } } while (true); ```

Performance: transactions add 20-50ms overhead due to conflict detection. Use them sparingly.

If you're on MongoDB 4.2+, you can also use distributed transactions across sharded clusters. But the latency penalty is higher — expect 100-200ms per transaction. Only do this when the business requirement genuinely demands cross-shard atomicity.

transfer_funds.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
namespace Io\TheCodeForge\MongoDb;

$client = ConnectionManager::getClient();
$session = $client->startSession();
$session->startTransaction([
    'readConcern' => new \MongoDB\Driver\ReadConcern(\MongoDB\Driver\ReadConcern::SNAPSHOT),
    'writeConcern' => new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY)
]);
try {
    $accounts = $client->selectDatabase('bank')->selectCollection('accounts');
    $accounts->updateOne(
        ['_id' => new \MongoDB\BSON\ObjectId($fromId)],
        ['$inc' => ['balance' => -100]],
        ['session' => $session]
    );
    $accounts->updateOne(
        ['_id' => new \MongoDB\BSON\ObjectId($toId)],
        ['$inc' => ['balance' => 100]],
        ['session' => $session]
    );
    $session->commitTransaction();
} catch (\Exception $e) {
    $session->abortTransaction();
    error_log('Transaction failed: ' . $e->getMessage());
}
Output
Atomic funds transfer with rollback on failure.
Transaction Mental Model
  • startSession() creates a logical session
  • startTransaction() opens a transaction within that session
  • All operations must include the session option
  • commitTransaction() writes all changes atomically
  • abortTransaction() undoes all changes in that transaction
Production Insight
A team used transactions for every write — 10x slow down.
Transactions add conflict detection overhead and wait for majority commit.
For simple field increments, $inc is atomic and 5x faster.
Rule: use transactions for multi-document consistency only; atomic operators for everything else.
Also remember that transactions have a 60s lifetime — if you have a long-running session, it will be killed. Keep them short.
Key Takeaway
Transactions need replica sets and session objects.
Use atomic operators for single-document updates.
Always set readConcern and writeConcern inside transactions.
Keep transactions short (<60s).
When to Use Transactions
IfSingle document update?
UseUse atomic operators ($set, $inc) — no transaction needed
IfMultiple documents, same collection?
UseUse findOneAndUpdate or bulkWrite with ordered => true
IfMultiple documents across collections?
UseUse transactions — they guarantee atomicity

Change Streams: Real-Time Data Feeds with PHP

MongoDB change streams allow you to subscribe to real-time changes on a collection, database, or entire deployment. They're built on the oplog and provide a reliable, ordered stream of events. In PHP, you use the MongoDB\Collection::watch() method to start listening.

Change streams are ideal for
  • Real-time dashboards that update when data changes
  • Cross-service synchronisation (e.g., push changes to Elasticsearch)
  • Audit logging of all modifications
  • Cache invalidation triggers

```php <?php namespace Io\TheCodeForge\MongoDb;

$orderCollection = ConnectionManager::getClient() ->selectDatabase('ecommerce') ->selectCollection('orders');

$resumeToken = $metadataCollection->findOne(['_id' => 'changeStreamResume'])['token'] ?? null; $options = ['maxAwaitTimeMS' => 1000]; if ($resumeToken) { $options['startAfter'] = $resumeToken; }

$pipeline = [['$match' => ['operationType' => 'insert']]]; $cursor = $orderCollection->watch($pipeline, $options);

foreach ($cursor as $event) { $order = $event['fullDocument']; // Send to a queue, update cache, etc. echo "New order: " . $order['_id'] . PHP_EOL; // Persist resume token $metadataCollection->updateOne( ['_id' => 'changeStreamResume'], ['$set' => ['token' => $event['_id']]], ['upsert' => true] ); } ```

Important production considerations
  • Change streams require a replica set (not a standalone).
  • The cursor blocks waiting for events. Use maxAwaitTimeMS to control polling interval.
  • If the cursor is idle for too long (default 10 minutes), MongoDB may close it. Reconnect gracefully.
  • $fullDocument: 'updateLookup' option gives you the full document after an update, but it adds a round-trip per event.
  • In sharded clusters, change streams are ordered within each shard but not globally. Use startAfter to resume from a specific point after a crash.

Change streams don't work with transactions — you can't see uncommitted changes. That's fine, because you only want to react to committed data anyway.

watch_orders.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace Io\TheCodeForge\MongoDb;

$orderCollection = ConnectionManager::getClient()
    ->selectDatabase('ecommerce')
    ->selectCollection('orders');

$pipeline = [['$match' => ['operationType' => 'insert']]];
$cursor = $orderCollection->watch($pipeline, ['maxAwaitTimeMS' => 1000]);

foreach ($cursor as $event) {
    $order = $event['fullDocument'];
    // Send to a queue, update cache, etc.
    echo "New order: " . $order['_id'] . PHP_EOL;
}
Output
New order: 6612a3b4c5d6e7f8g9h0i1j2
New order: 6612a3b4c5d6e7f8g9h0i1j3
... (continuously)
Resume Tokens
Change streams provide resume tokens. Save the _id field of the last processed event and pass it as startAfter when restarting the stream. This ensures exactly-once processing semantics — critical for payment integrations.
Production Insight
A team ran a change stream in a long-running PHP process that died after 30 minutes due to the default idle timeout.
The stream didn't resume from where it left off — they lost events.
Fix: save the resume token periodically and use startAfter on restart.
Rule: always implement resume logic with stored tokens when using change streams.
Key Takeaway
Change streams need replica sets.
Use $match to filter operation types.
Save resume tokens for reliable replay.
Set maxAwaitTimeMS to control polling.

Replica Set Configuration and Failover Handling

A MongoDB replica set provides automatic failover and data redundancy. The PHP driver discovers replica set members from the connection string and routes writes to the primary. But proper configuration is critical — missteps can cause silent failover delays or split-brain scenarios.

Connection string: Use the SRV format for automatic discovery: mongodb+srv://user:pass@cluster0.example.mongodb.net/db?retryWrites=true&w=majority. This tells the driver to query DNS for all replica set members.

Read preference: Set to secondaryPreferred for read-heavy workloads, but be aware of stale reads. If you read-after-write, use primary or enable causal consistency.

Retry writes: Always enable retryWrites=true in the connection string. This re-runs write operations if the primary steps down during a write. It doesn't replace write concern, but adds resilience.

Failover test script:

```php <?php namespace Io\TheCodeForge\MongoDb;

$uri = 'mongodb+srv://user:pass@cluster0.example.mongodb.net/db?retryWrites=true&w=majority'; $client = new MongoDB\Client($uri);

$collection = $client->selectDatabase('test')->selectCollection('failover');

// Step down the primary (requires admin) $admin = $client->selectDatabase('admin'); $admin->command(['replSetStepDown' => 60, 'force' => true]);

// Try to write immediately — should retry automatically $collection->insertOne(['test' => 'failover', 'ts' => new MongoDB\BSON\UTCDateTime()]); echo "Write succeeded after failover. "; ```

Always test failover in a staging environment. Simulate primary step-down, network partitions, and secondary lag before going live.

failover_test.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace Io\TheCodeForge\MongoDb;

$uri = 'mongodb+srv://user:pass@cluster0.example.mongodb.net/db?retryWrites=true&w=majority';
$client = new MongoDB\Client($uri);

$collection = $client->selectDatabase('test')->selectCollection('failover');

// Step down the primary (requires admin)
$admin = $client->selectDatabase('admin');
$admin->command(['replSetStepDown' => 60, 'force' => true]);

// Try to write immediately — should retry automatically
$collection->insertOne(['test' => 'failover', 'ts' => new MongoDB\BSON\UTCDateTime()]);
echo "Write succeeded after failover.\n";
Output
Write succeeded after failover.
Failover Write Concern Gotcha
If you have w:majority but a minority of replicas are down, writes will block until wtimeoutMS. Set a reasonable wtimeoutMS (e.g., 1000ms) to fail fast instead of hanging infinitely. Also, if the primary goes down, the driver will retry automatically with retryWrites=true.
Production Insight
A team didn't set retryWrites=true and faced 5-minute outages during primary elections.
Without retryWrites, writes fail immediately when the primary steps down.
After enabling retryWrites, the driver automatically retries writes on the new primary within milliseconds.
Rule: Always set retryWrites=true and w=majority in production connection strings.
Additionally, monitor election times — if elections take >10s, your cluster has underlying issues.
Key Takeaway
Use SRV connection strings for automatic discovery.
Always set retryWrites=true.
Test failover scenarios in staging.
Set wtimeoutMS to avoid blocking during minority failures.
Failover Read Strategy Decision
IfImmediate read-after-write consistency required
UseUse readPreference primary and writeConcern majority + retryWrites
IfRead-heavy workload, okay with eventual consistency
UseUse readPreference secondaryPreferred
IfNeed causal consistency across operations
UseEnable causal consistency in the session

Handling Schema Migrations Without Downtime

You shipped MongoDB because you thought 'schemaless' meant you'd never touch migrations again. Wrong. Schemaless just means the database won't scream at you, but your application will silently corrupt data when you add a field that old documents don't have. Real downtime happens when a new code path expects a field that doesn't exist in production documents from six months ago.

The fix: write defensive read paths with $exists checks or use MongoDB's schema validation with $jsonSchema. But the real trick is incremental migrations. Never update all documents at once. Run a background script that iterates with a cursor noBatchSize and a write concern of acknowledged. Add the new field with a default, then update your application to read both old and new shapes. Only after you've confirmed zero read errors do you drop the fallback logic. This is how you avoid a 3 AM outage because your aggregation pipeline exploded on a null field that doesn't exit gracefully.

IncrementalMigration.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — php tutorial

$collection = (new MongoDB\Client)->inventory->products;

// Step 1: Add new field with default to documents that lack it
$filter = ['warranty_years' => ['$exists' => false]];
$update = ['$set' => ['warranty_years' => 1]];
$options = ['batchSize' => 500, 'writeConcern' => new MongoDB\Driver\WriteConcern(1)];

$cursor = $collection->find($filter, $options);
foreach ($cursor as $doc) {
    $collection->updateOne(
        ['_id' => $doc['_id']],
        ['$set' => ['warranty_years' => 1]]
    );
    // Log progress for rollback tracking
    error_log('Migrated: ' . $doc['_id']);
}

// Output:
// Migrated: 66d8e2f4a1b2c3d4e5f6a7b8
// Migrated: 66d8e2f4a1b2c3d4e5f6a7b9
// ...
// (script logs each migrated document ID)
Output
Migrated: 66d8e2f4a1b2c3d4e5f6a7b8
Migrated: 66d8e2f4a1b2c3d4e5f6a7b9
...
Production Trap: The Rollback Nightmare
If you batch update a million documents and your new field breaks something, unwinding is hell. Always run incremental updates on a sub-1000 document batch first, test the application path, then scale. Keep a log of which documents were migrated so you can reverse the update if needed.
Key Takeaway
Add fields with defaults first, then update reads; never update all documents at once without a rollback plan.

Time-Series Data: Writes That Won't Burn Your IO Budget

MongoDB time-series collections are not a magic bullet. They're a specialized tool that forces you to think about data granularity before you write a single line of PHP. The default approach—write one document per event per second—will destroy your write throughput and blow up your storage costs. Why? Because every write triggers an index update, and with millions of small documents, you're paying for metadata overhead.

The right move: bucket your data. MongoDB time-series collections do this automatically, but only if you define the metaField and granularity correctly. Set granularity to 'seconds' if you're logging IoT sensor data every tick. Use 'minutes' for aggregated metrics like API response times. If you don't, MongoDB will guess and likely get it wrong, forcing your queries to scan more buckets than necessary. Then you'll wonder why your $avg aggregation on the last hour takes eight seconds.

Here's the pattern: group your measurements by sensor_id as the metaField, then let MongoDB's internal bucketing compress writes. Your PHP code just inserts flat documents—MongoDB handles the compression. But remember: a poorly chosen granularity means your data stays uncompressed. Profiling your query patterns before picking granularity saves you a rewrite.

TimeSeriesWrite.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — php tutorial

// Create time-series collection once
$db = (new MongoDB\Client)->iot_db;
$db->createCollection('sensor_readings', [
    'timeseries' => [
        'timeField' => 'timestamp',
        'metaField' => 'sensor_id',
        'granularity' => 'seconds'
    ]
]);

$collection = $db->sensor_readings;

// Insert raw events — MongoDB handles bucketing
$batch = [
    ['timestamp' => new MongoDB\BSON\UTCDateTime(), 'sensor_id' => 'sensor_alpha', 'temperature' => 72.3],
    ['timestamp' => new MongoDB\BSON\UTCDateTime(), 'sensor_id' => 'sensor_alpha', 'temperature' => 72.1],
    ['timestamp' => new MongoDB\BSON\UTCDateTime(), 'sensor_id' => 'sensor_beta', 'temperature' => 68.9]
];

$result = $collection->insertMany($batch);
echo 'Inserted ' . $result->getInsertedCount() . ' events';

// Output:
// Inserted 3 events
Output
Inserted 3 events
Senior Shortcut: Granularity Selection
Match granularity to your measurement interval: seconds for high-frequency sensors, minutes for application metrics like endpoint latency. If you're unsure, set it to 'seconds'—it's the safest bet and can be tailored later with collMod options. But never omit it; default will burn you.
Key Takeaway
Always set time-series granularity explicitly; the default will compress poorly and kill query performance.

Bulletproof Error Handling: Don't Let a Dead MongoDB Takedown Your App

Production MongoDB goes down. Drivers throw exceptions. Network partitions happen. The difference between a senior dev and a junior is how gracefully you handle that moment. You catch, log, circuit-break, and retry — not bury errors in a try-catch with a generic 'something went wrong'.

MongoDB PHP driver throws MongoDB\Driver\Exception\ConnectionTimeoutException when it can't reach the server. Your code must treat this as a first-class failure path. Implement exponential backoff. Use a health-check loop before touching the database. Log the full error context including server host and operation.

Don't let a missing database bring down your entire page. Catch specific exceptions, present a stale cache, and alert ops. Your users forgive a stale dashboard. They don't forgive a 500 error.

resilient_query.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — php tutorial

function fetchUserOrders(string $userId, int $retries = 3): array {
    $attempt = 0;
    while ($attempt < $retries) {
        try {
            $collection = (new MongoDB\Client())->ecommerce->orders;
            return $collection->find(['user_id' => $userId])->toArray();
        } catch (MongoDB\Driver\Exception\ConnectionTimeoutException $e) {
            $attempt++;
            error_log("MongoDB timeout: {$e->getMessage()} (attempt {$attempt})");
            if ($attempt >= $retries) {
                throw $e;
            }
            usleep(100000 * pow(2, $attempt)); // exponential backoff: 200ms, 400ms, 800ms
        }
    }
    return [];
}

try {
    $orders = fetchUserOrders('abc123');
} catch (\Throwable $e) {
    http_response_code(503);
    echo json_encode(['error' => 'Service temporarily unavailable']);
}
Output
On failure: HTTP 503 with error JSON. On success: array of order documents.
Production Trap:
Catching a generic \Exception swallows bugs like syntax errors. Always catch the most specific MongoDB exception type first. Let unexpected exceptions bubble up to your global exception handler—don't silence them in a query function.
Key Takeaway
Always catch specific MongoDB driver exceptions, implement retry with backoff, and fail gracefully with a stale cache or 503 — never a blank white page.

Bulk Writes at Scale: Batch Your Operations or Eat the Latency

Sending one INSERT per document kills throughput. MongoDB PHP driver gives you bulkWrite(). Use it. A single network round-trip can insert 10,000 documents instead of 10,000 separate calls. That's the difference between 2 seconds and 2 minutes.

Bulk writes support inserts, updates, deletes in one batch. Ordered mode stops on first error — useful for sequential operations. Unordered mode runs all operations even if some fail — perfect for loading logs where a few duplicates don't matter.

Batch size matters. MongoDB can't handle an infinite payload. Cap your batches at 10,000 operations or 16MB of data per batch, whichever hits first. Split your array into chunks. Use array_chunk() in PHP to keep your memory sane.

Why wait for each document? Fire a bulk write and move on. Your API response times will thank you.

bulk_insert.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — php tutorial

$collection = (new MongoDB\Client())->analytics->pageviews;
$records = [];
for ($i = 0; $i < 20000; $i++) {
    $records[] = ['page' => '/product/' . $i, 'timestamp' => new MongoDB\BSON\UTCDateTime()];
}

$chunks = array_chunk($records, 5000);
foreach ($chunks as $batch) {
    $operations = [];
    foreach ($batch as $doc) {
        $operations[] = ['insertOne' => [$doc]];
    }
    $result = $collection->bulkWrite($operations, ['ordered' => false]);
    echo "Inserted: {$result->getInsertedCount()}\n";
}
Output
Inserted: 5000
Inserted: 5000
Inserted: 5000
Inserted: 5000
Total: 20000 records inserted in 4 batches.
Senior Shortcut:
Use ordered => false for high-throughput ingestion where order doesn't matter. It avoids a single bad document blocking the entire batch. Error handling is on you — check getWriteErrors() on the result object after each batch.
Key Takeaway
Always batch writes into bulk operations capped at 10,000 docs or 16MB — one network call beats a thousand in production latency.
● Production incidentPOST-MORTEMseverity: high

The Silent Write Failure: When MongoDB Accepts Your Data and Then Loses It

Symptom
After a replica set election, documents that were successfully inserted are gone. No errors during write operations. Application logs show successful responses from MongoDB.
Assumption
The team assumed that a successful write response guarantees persistence. They didn't realise that without explicit write concern, MongoDB acknowledges the write after the primary captures it in memory — before replication is confirmed.
Root cause
Default write concern is {w: 1}, meaning the primary acknowledges the write locally before replicating to secondaries. If the primary crashes before replication completes, the write is lost. The team never set w: majority.
Fix
Set write concern to majority on all critical writes: $collection->insertOne($doc, ['writeConcern' => new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY)]);. For high-durability workloads, also enable journaling: j: true.
Key lesson
  • Never trust default write concern for business-critical data.
  • Always set w: majority and j: true for financial or user-signup data.
  • Test failover scenarios before going to production — don't learn this during an outage.
  • Use retryWrites=true to automatically re-attempt writes after a primary stepdown — it's not a substitute for proper write concern, but it reduces the window for data loss.
Production debug guideSymptom → Action guide for the five most common production failures5 entries
Symptom · 01
Connection timeout: Cannot connect to MongoDB server
Fix
Check network connectivity: telnet <host> 27017. Verify firewall rules. Ensure MongoDB is running and reachable. Check connection string for typos. Test with mongosh --host <host> from the app server.
Symptom · 02
Authentication failed: Unauthorized to execute command
Fix
Verify username, password, and authSource in the connection string. Ensure the user has the correct role on the target database. Check that authentication is enabled on the MongoDB side (security.authorization: enabled).
Symptom · 03
Slow queries (>100ms) despite indexed fields
Fix
Run db.collection.explain('executionStats').find(...) to check if MongoDB is using the expected index. Look for COLLSCAN as a sign of missing index. Check for query patterns that don't match index prefix. Use hint() to force an index temporarily.
Symptom · 04
Cursor timeout: Please reconnect or use maxTimeMS``
Fix
Aggregation pipelines or large result sets hold cursors open too long. Set maxTimeMS on the command. Use batchSize to control each batch. Avoid processing large datasets one document at a time — batch updates instead.
Symptom · 05
BSON serialisation error: Type mismatch or unsupported type
Fix
MongoDB expects BSON types (e.g., ObjectId, DateTime). PHP's mongodb/mongodb library automatically converts most types, but issues arise with custom classes or large integers. Ensure integers fit in BSON's 64-bit signed range. Use new MongoDB\BSON\UTCDateTime() for dates. Manually convert with MongoDB\BSON\fromPHP() and debug.
★ Quick Debug Cheat Sheet: PHP + MongoDBFive-second commands to diagnose the most disruptive production issues.
Connection refused
Immediate action
Check if MongoDB server is reachable from app host
Commands
mongosh --host <host> --port 27017
telnet <host> 27017
Fix now
Update connection string in .env or config, ensure network ACL allows app server
Write succeeded but data lost after failover+
Immediate action
Check current write concern in driver config
Commands
echo $collection->getWriteConcern()
db.adminCommand({getParameter: 1, w: 1})
Fix now
Set writeConcern to majority and j: true in MongoDB\Driver\Manager options
Aggregation pipeline times out+
Immediate action
Check pipeline stages for $lookup or $unwind on large collections
Commands
db.collection.aggregate([...], {explain: true})
db.collection.aggregate([...], {allowDiskUse: true, maxTimeMS: 10000})
Fix now
Add indexes on foreign fields used in $lookup, limit dataset with $match early
Cursor batch delay between results+
Immediate action
Check batchSize default (101 documents per batch)
Commands
db.collection.find().batchSize(500)
db.runCommand({getMore: cursorId, batchSize: 500})
Fix now
Increase batchSize for bulk reads, reduce round trips
ObjectId _id generation duplicates under high concurrency+
Immediate action
Verify using MongoDB\BSON\ObjectId from mongodb/mongodb
Commands
check if you are generating _id manually as integer
check for race conditions in upsert with _id
Fix now
Let MongoDB generate _id automatically; avoid custom sequential ids
Comparison of PHP + MongoDB vs MySQL + ORM
AspectPHP + MongoDBMySQL + ORM (e.g., Doctrine)
Schema flexibilitySchemaless — add fields on the flyRequires migrations to change schema
JoinsNo native joins; use $lookup in aggregation (expensive)Native joins via SQL, optimizer-driven
Query languageMQL (MongoDB Query Language) as PHP arraysSQL, wrapped by ORM DQL
TransactionsMulti-document transactions since 4.0 (limited overhead)Mature ACID transactions
IndexingB-tree, text, geospatial, TTL indexesB-tree, full-text, spatial (varies by engine)
PHP integrationMongodb/mongodb library, ext-mongodb C extensionPDO/MySQLi or ORM libs
Best forRapidly evolving schemas, nested data, high write throughputComplex relationships, reporting, strong consistency

Key takeaways

1
MongoDB schemaless model fits flexible data but requires discipline in versioning and index design.
2
PHP's MongoDB driver performance depends on connection pooling
adjust maxPoolSize per FPM concurrency.
3
Use writeConcern majority and journal true for critical writes; test failover scenarios.
4
Aggregation pipelines
place $match first, index foreign keys for $lookup, set allowDiskUse and maxTimeMS.
5
Explain every query before production
look for IXSCAN, avoid COLLSCAN.
6
Limit indexes on write-heavy collections and monitor usage with $indexStats.
7
Use atomic operators instead of transactions for single-document updates; transactions are for multi-document consistency only.
8
Change streams need replica sets and resume token persistence for reliable real-time feeds.
9
Always set retryWrites=true and timeouts in connection options to handle outages gracefully.

Common mistakes to avoid

7 patterns
×

Not setting write concern to majority

Symptom
Data loss after replica set failover — writes appear successful but vanish when primary goes down.
Fix
Set w: 'majority' as default in the connection URI options, and j: true for critical data.
×

Using fine-grained collections instead of embedded documents

Symptom
N+1 query problem: fetching a user and their addresses requires separate queries, just like in SQL.
Fix
Embed related data (e.g., address as an array inside user document) unless the relationship is truly many-to-many and large.
×

Ignoring the 16MB document size limit

Symptom
Insert fails with 'Document too large' error when storing arrays of sub-documents that push over 16MB.
Fix
Use GridFS for files or break large arrays into separate collections referenced by _id.
×

Relying on the default read preference 'primary' for read-heavy workloads

Symptom
Primary server CPU at 90% while secondaries are idle; queries get slower under load.
Fix
Set readPreference to 'secondaryPreferred' for read-only queries. But be aware of stale data — use 'primary' for read-after-write consistency.
×

Not using BSON type classes for dates and ObjectIds

Symptom
Dates stored as strings, ObjectIds as integers — queries fail to match because types differ.
Fix
Always use MongoDB\BSON\UTCDateTime for dates, MongoDB\BSON\ObjectId for _id references.
×

Overusing transactions for single-document updates

Symptom
Write latency jumps from 5ms to 60ms for every update, transaction conflict errors under load.
Fix
Use atomic operators ($inc, $set, $push) for single-document updates. Reserve transactions for multi-document consistency only.
×

Forgetting to set socket and connection timeouts

Symptom
PHP-FPM workers pile up waiting for a slow or dead MongoDB node, eventually exhausting the process pool.
Fix
Always set connectTimeoutMS (e.g., 3000) and socketTimeoutMS (e.g., 10000) in the connection options.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How do you handle schema evolution in MongoDB when using PHP?
Q02SENIOR
Explain how MongoDB's PHP driver handles connection pooling. What can go...
Q03SENIOR
Write a PHP script that demonstrates a safe update using a write concern...
Q04SENIOR
What are the main performance considerations when using aggregation pipe...
Q05SENIOR
How would you implement a change stream consumer in PHP that survives re...
Q06SENIOR
Describe the impact of using 'secondaryPreferred' read preference on dat...
Q01 of 06SENIOR

How do you handle schema evolution in MongoDB when using PHP?

ANSWER
MongoDB doesn't enforce a schema, but your application does. Use a version field in each document to track schema versions. In your PHP code, check the version and transform documents on read. For example, if a new field phone was added, missing it returns null instead of an error. For complex migrations, write a script that iterates through documents and updates them using bulkWrite with $set. Never use findAndModify for bulk updates — it processes one document at a time. ``php $bulk = []; foreach ($collection->find(['schema_version' => 1]) as $doc) { $bulk[] = [ 'updateOne' => [ ['_id' => $doc['_id']], ['$set' => ['schema_version' => 2, 'phone' => null]] ] ]; if (count($bulk) >= 100) { $collection->bulkWrite($bulk); $bulk = []; } } if ($bulk) $collection->bulkWrite($bulk); `` Always test migrations on a replica set member off the critical path.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What is PHP and MongoDB in simple terms?
02
How do I install the PHP MongoDB driver for production?
03
Can I use MongoDB and MySQL together in the same PHP project?
04
What is the maximum document size in MongoDB? How do I handle larger data?
05
Why are my MongoDB queries slow despite creating indexes?
06
When should I use transactions instead of atomic operators?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's PHP & MySQL. Mark it forged?

13 min read · try the examples if you haven't

Previous
SQL Injection Prevention in PHP
5 / 6 · PHP & MySQL
Next
Database Transactions in PHP