Stop Sending Your Whole Meteor Collection to Every Browser
Scope Meteor publications with selectors and limits so clients sync only the data they actually need.
05 Jul 2025, 14:32 UTC

The problem: a publication that never says no
Imagine a task board built with Meteor. The server publishes the tasks collection like this:
Meteor.publish('tasks', function () {
return Tasks.find();
});
That code looks harmless in a small prototype. This assumes Meteor 2.x or 3.x with the standard DDP publish/subscribe API. In production, Tasks.find() with no selector returns every document in the collection. Meteor’s publish/subscribe system then streams those documents over DDP—the Distributed Data Protocol that Meteor uses over WebSocket—to every connected browser. Each browser stores a local copy in Minimongo, Meteor’s client-side in-memory collection, and any change to any task triggers reactive UI updates for every subscriber.
As the collection grows, users notice slower page loads, higher memory use, and occasional UI freezes when a background edit causes dozens of templates to re-render.
The engineering decision: scope what the client receives
Instead of publishing everything, define a publication that accepts a selector and pagination options. This keeps the client’s Minimongo small and limits DDP traffic to documents the current view actually displays.
Server-side publication
// server/publications/tasks.js
Meteor.publish('tasks.byProject', function (projectId, options = {}) {
check(projectId, String);
check(options, Object);
const selector = { projectId, archived: false };
const { limit = 50, sort = { createdAt: -1 } } = options;
return Tasks.find(selector, { limit, sort });
});
check validates arguments before they reach the database. The selector restricts documents to one project and excludes archived tasks. limit caps the number of documents sent over the wire, and sort gives the client a deterministic order.
Client-side subscription
// client/subscriptions/tasks.js
Tracker.autorun(() => {
const projectId = Session.get('activeProject');
if (projectId) {
Meteor.subscribe('tasks.byProject', projectId, {
limit: 50,
sort: { createdAt: -1 },
});
}
});
Tracker.autorun re-runs whenever Session.get('activeProject') changes, so switching projects unsubscribes from the old stream and subscribes to the new one. This pattern keeps the client collection scoped to the active context.
Worked example: verify that only filtered data arrives
- Run
meteor create taskboardin a terminal, then add the publication and subscription files above. - Start the app locally with
meteor run. This is a development command; do not use it for production traffic. - Open Chrome DevTools, go to the Network tab, and filter by
WebSocketto inspect DDP frames. - Insert more than 100 tasks into the database, but only 10 should match
projectId. - Subscribe to
tasks.byProjectand inspect the DDPaddedmessages. Expect roughly ten document additions, not the full collection count. - Open the browser console and run
Tasks.find().count(). It should match the number ofaddedmessages, not the server-side total.
If the console count equals the full collection size, the selector is not being applied or the subscription is missing arguments.
Trade-offs and limitations
- Reactivity can still cost CPU. Even a filtered publication pushes every insert, update, and removal to the client. A high-frequency collection—such as live sensor readings—can cause
Tracker.autoruncallbacks and template re-renders more often than necessary. Consider batching server updates or throttling UI computations for fast-changing data. - One WebSocket per client. Meteor’s DDP connection runs over a single WebSocket. Large publications can block smaller messages behind them, so a big initial payload delays later updates.
- Node.js is single-threaded. The Meteor server handles DDP connections on Node’s event loop. Many concurrent subscriptions to large datasets can saturate CPU before network bandwidth becomes the issue. Horizontal scaling behind a load balancer with sticky sessions is the standard remedy, though it adds operational complexity.
- Avoid
publishCompositeunless needed. It simplifies joining related collections but adds nesting complexity and can create circular publication dependencies that are hard to debug.
Practical check before you deploy
After deploying to staging, open the Network tab while navigating through the app. Record the DDP frame size for a typical user journey. If a single subscription delivers thousands of documents, add a limit or narrow the selector. Repeat the check after any collection grows past a few hundred active documents.
Rollback
Changing a publication is a code change, not a data migration. To roll back, revert the server file to the previous publication and redeploy. Client subscriptions that reference the old function name will fail silently until updated, so keep the publication name stable or update both sides in the same deploy.
Scoped publications are not a complete performance strategy, but they are the first filter between your database and your users’ browsers. Start with the selector, measure the DDP stream, and add limits before the collection outgrows the client.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.