Meteor server MongoDB pool exhaustion under subscription load: oplog vs polling and observer duplication
0 reputation · 13 Jun 2020, 17:52 UTC
0 reputation · 13 Jun 2020, 17:52 UTC
A Meteor application backed by MongoDB intermittently stalls under load: requests time out and the server appears to hang, with symptoms correlating with the number of active DDP subscriptions rather than raw query volume. The goal is to diagnose whether the exhaustion comes from the MongoDB driver's connection pool being saturated by server-side observe handles, and to decide on the right corrective direction.
Relevant constraints: Meteor opens MongoDB connections through the bundled Node MongoDB driver, so pool sizing (e.g., maxPoolSize) is set via driver options on the connection string, not a Meteor-specific API, and defaults vary with the driver version pinned to the Meteor release in use. Oplog tailing via MONGO_OPLOG_URL is supported but requires replica set oplog credentials that some managed MongoDB tiers do not expose; without it, Meteor falls back to poll-and-diff, which issues repeated queries. Observe handles for similar queries may not be multiplexed across clients when publications differ in selectors or options, and reconnect storms or hot code pushes can spike observer creation.
Given that, the open questions are:
35275 reputation · 14 Jun 2020, 01:56 UTC
The pool exhaustion is almost certainly caused by observer duplication from non-multiplexed publications combined with poll-and-diff query storms when oplog tailing is unavailable. Each unique publication cursor (different selector, sort, options, or fields) creates a separate observe handle that holds a driver connection for its lifetime. Under subscription load, this scales connections linearly with clients rather than logarithmically. The fix priority is: (1) restore oplog access if your MongoDB tier allows it, (2) consolidate publications to enable multiplexing, (3) tune polling interval only if oplog cannot be restored, (4) increase maxPoolSize only as a last resort after verifying the above.
maxPoolSize is set via connection string (e.g., mongodb://host/db?maxPoolSize=100). Meteor 2.x pins driver 3.6 (default pool=5); Meteor 3.x pins driver 6.x (default pool=100). Check .meteor/versions and package.json.MONGO_OPLOG_URL, Meteor falls back to poll-and-diff every _pollInterval (default 10s), issuing repeated find queries per observe handle.const { MongoClient } = require('mongodb');
const client = new MongoClient(process.env.MONGO_URL);
client.on('connectionPoolCreated', pool => {
pool.on('connectionCheckedOut', () => metrics.increment('pool.checked_out'));
pool.on('connectionCheckedIn', () => metrics.decrement('pool.checked_out'));
pool.on('connectionPoolClosed', () => metrics.set('pool.checked_out', 0));
});
// Expose /metrics or log every 5spool.checked_out ≈ active subscriptions and wait-queue depth grows while Node CPU < 30%, the driver pool is the bottleneck.db.setProfilingLevel(1, {slowms: 50}); count find queries on published collections per minute. Polling shows repeated identical queries; oplog shows only getMore on oplog cursor.Mongo.Collection._publishCursor (server-only):const origPublishCursor = Mongo.Collection.prototype._publishCursor;
Mongo.Collection.prototype._publishCursor = function(cursor, sub, name) {
const key = JSON.stringify({
collection: this._name,
selector: cursor.selector,
options: cursor.options
});
console.log('[OBSERVE]', name, 'keyHash:', require('crypto').createHash('md5').update(key).digest('hex').slice(0,8));
return origPublishCursor.call(this, cursor, sub, name);
};db.currentOp() or driver cursor events). Distinct cursorIds = distinct observe handles.| Action | When It Helps | Verification |
|---|---|---|
Restore oplog (MONGO_OPLOG_URL) | Managed tier exposes oplog (Atlas M10+, self-hosted replica set) | Set MONGO_OPLOG_URL, restart; verify Meteor._OplogDriver.isOplogEnabled() === true and poll queries disappear from profiler |
| Consolidate publications | Selectors differ only by userId (use this.userId in single publication) | Load test N subscriptions to same publication; pool checked-out should stay at 1, not N |
| Reduce polling interval | Oplog unavailable; Meteor._setPollInterval(30000) (30s) reduces query rate | Profiler: find queries/min drops proportionally; _pollingThrottle only batches DDP messages, not server queries |
Increase maxPoolSize | After above steps, if pool wait-queue still > 0 at peak | Raise to 100-200; confirm wait-queue depth ≈ 0 and p99 checkout latency < 10ms under load |
What is your MongoDB deployment type and tier? (Atlas M0/M2/M5/M10+, self-hosted replica set, Cosmos DB, DocumentDB, etc.) This determines whether oplog access is possible and changes the entire priority order. If oplog is available, do that first and skip polling tuning. If not, publication consolidation becomes the primary lever.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.