Meteor's DDP and Pub/Sub: When Real-Time Sync Becomes a Liability
Meteor's DDP and pub/sub make real-time easy, but every subscription mirrors data into client memory and forces server diff computation. Learn to narrow publications with field projections and filters, and when to swap subscriptions for REST calls.
28 Sept 2026, 06:03 UTC

The problem: real-time by default isn't free
Meteor's Distributed Data Protocol (DDP) and publication/subscription model make real-time synchronization trivial to enable. A single Meteor.publish on the server and Meteor.subscribe on the client gives you live updates across every connected browser. The catch is that every subscription opens a persistent WebSocket, mirrors documents into client-side MiniMongo, and forces the server to compute and push diffs on every write. For a dashboard with a few hundred records this is fine. For a feed with 50,000 documents and 200 concurrent users, the memory and CPU costs become the bottleneck.
Takeaway: Treat DDP subscriptions as a scarce resource. Publish only the fields and documents the current view actually needs, and consider REST or GraphQL for read-heavy, non-collaborative screens.
How the publication/subscription loop works
When a client calls Meteor.subscribe('activeTasks'), the server runs the publish function, cursors the relevant MongoDB collection, and sends an initial batch of documents over the DDP WebSocket as added messages. The client stores them in MiniMongo—a client-side MongoDB clone—so UI components can query synchronously with Tasks.find(). Subsequent writes on the server trigger changed or removed DDP messages containing only the delta. The client applies those deltas to MiniMongo, and reactive helpers like Tracker.autorun re-run automatically.
This architecture separates data fetching from rendering. The UI never waits on a network round-trip for reads; it reads from the local cache. The trade-off is that the cache size equals the sum of all active subscriptions.
Worked example: narrowing a publication with field projection and filters
Suppose a project-management board shows only the task title, status, and assignee for the current sprint. A naive publication sends the entire document, including description, comments, and attachment metadata.
// server/publications.js
Meteor.publish('sprintTasks', function (sprintId) {
if (!this.userId) return this.ready();
const selector = { sprintId, archived: { $ne: true } };
const options = { fields: { title: 1, status: 1, assigneeId: 1 } };
return Tasks.find(selector, options);
});On the client, subscribe in the route or component lifecycle:
// client/pages/BoardPage.jsx
import { useTracker } from 'meteor/react-meteor-data';
import { Meteor } from 'meteor/meteor';
import { Tasks } from '/imports/api/tasks';
export function BoardPage({ sprintId }) {
const tasks = useTracker(() => {
Meteor.subscribe('sprintTasks', sprintId);
return Tasks.find({ sprintId }, { sort: { order: 1 } }).fetch();
}, [sprintId]);
// render board columns...
}Verification: Open the browser Network tab, filter for WS frames, and confirm the initial added messages contain only the three projected fields. Modify a task's description in the MongoDB shell (db.tasks.updateOne({_id: ...}, {$set: {description: 'new'}})) and observe that no DDP changed message is sent for that field.
Where the model strains
- Client memory: MiniMongo holds every subscribed document in memory. A subscription returning 10,000 documents with 20 fields each can consume 50–100 MB per tab.
- Server CPU: Each write triggers a diff computation for every subscription that matches the changed document. High-write collections (audit logs, chat messages) amplify this cost.
- Connection limits: Each tab holds one WebSocket. Browsers limit concurrent connections per host; multiple tabs can starve other resources.
Mitigations include pagination via limit/skip in the publish cursor, using disableOplog for high-churn collections, and offloading read-only views to a REST endpoint that bypasses DDP entirely.
Actionable next step
Audit your top five subscriptions by document count and field count. For each, ask: does the current view need every field? Can the filter be tighter? Add a fields projection and a more selective selector, then re-measure WebSocket frame size in the Network tab. If a screen only needs a snapshot, replace the subscription with a one-time Meteor.call to a method that returns plain JSON.