Meteor Data Delivery: Choosing Between a Publication and a Method
Publications push live updates but cost scales with subscribers and observed documents; Methods are snapshots that scale per call. How to pick one per view, with a mirrored implementation and a validation path.
24 Aug 2026, 07:32 UTC

If a Meteor view renders the same documents for every signed-in user, you have to choose one owner for that data: a DDP publication that pushes changes to the client, or a Method whose result the client stores in a local reactive variable. The useful default is that publications earn their keep for small, live, per-user document sets, while Methods fit paginated lists, aggregations, and any read where you want the request to finish and stop consuming server memory. Mixing both for one collection is common, but only when you write down which path is authoritative.
The constraints that actually decide it
- Update model. A publication is a live query: the server keeps an observer on the cursor and pushes diffs over DDP (Distributed Data Protocol, Meteor's WebSocket-based data channel). A Method is a request/response call; its return value is a snapshot.
- Server cost shape. Subscription cost scales with subscribers multiplied by observed documents, not with request count. A Method's cost is per call and ends when the call returns.
- Security boundary. A publication exposes exactly the documents and fields it returns, so the filter must be built from
this.userId. A Method returns only to its caller, but still needs argument validation and an authorization check inside the method body. - Version. Meteor 3 moved server APIs to async/await. Meteor 2 examples and some packages will not run unchanged, so confirm behavior against the release you have installed.
Comparing the two delivery paths
| Concern | Publication + subscribe | Method + local state |
|---|---|---|
| Updates | Automatic push to all subscribers | None; client must re-call or invalidate |
| Scaling unit | Subscribers × observed documents | Calls per unit time |
| Pagination | Needs explicit limit or a custom reactive join | Straightforward with limit/skip or a cursor token |
| Latency compensation | Not applicable to reads | Applies to the method stub, not to returned data |
| Cache control | Weak; the server decides when to push | Strong; you decide when to refetch |
| Good fit | Dashboards, notifications, small per-user sets | Search results, reports, exports, writes |
Trade-offs that are easy to get wrong
Reactivity is not free
Every active subscription holds a server-side observer. Ten subscribers watching fifty documents each is a very different load profile from one subscriber watching five hundred. An unfiltered publication that returns a whole collection to every subscriber multiplies memory and CPU in a way that is invisible in development with one browser tab open.
Method results go stale
A Method that returns a list gives the client a snapshot. If another user edits one of those documents, the open view keeps showing the old value until something re-calls the method. That is acceptable for reports and exports, and wrong for a live inbox. If you need both, publish a small change signal (an id and a revision timestamp) and let the client re-call the method when the signal moves.
Reactive pagination needs an explicit limit
Skip-based paging over a publication is unstable under concurrent inserts, because rows shift between pages as documents are added. Either cap the publication with a fixed limit and sort key, or build a custom reactive join that tracks the ids you actually want. Do not rely on skip growing over time in a live subscription.
Concrete implementation: one collection, two paths
The example below assumes a Documents collection with an ownerId field, accounts configured so this.userId is populated, and MongoDB as the backing store. Run the server blocks on the server only, and confirm the cursor method names against your release's changelog before copying them.
// server/documents.js — server only
import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import { Documents } from '../imports/collections';
const MAX = 50;
Meteor.publish('documents.recent', async function ({ limit = 20 } = {}) {
check(limit, Match.Integer);
if (!this.userId) return this.ready(); // publish nothing when signed out
const capped = Math.min(Math.max(limit, 1), MAX);
return Documents.find(
{ ownerId: this.userId }, // authorization lives here
{ fields: { title: 1, updatedAt: 1 }, limit: capped, sort: { updatedAt: -1 } }
);
});
Meteor.methods({
async 'documents.recent'({ limit = 20 } = {}) {
check(limit, Match.Integer);
if (!this.userId) throw new Meteor.Error('not-authorized');
const capped = Math.min(Math.max(limit, 1), MAX);
return Documents.find(
{ ownerId: this.userId },
{ fields: { title: 1, updatedAt: 1 }, limit: capped, sort: { updatedAt: -1 } }
).fetchAsync(); // async cursor read in Meteor 3
},
});
On the client, keep the subscription handle and gate rendering on its readiness instead of assuming the data is present on first render:
// client/documents.js
const handle = Meteor.subscribe('documents.recent', { limit: 20 });
// With react-meteor-data, useTracker re-renders when handle.ready() flips.
// Render a loading state while !handle.ready(), then read from Documents.
If you keep both paths for the same collection, write the ownership rule into the code: for example, the publication is authoritative for the list view, and the Method is used only for exports and search, which never write back into the client collection.
Validating the choice
- In a scratch app, add both paths for one collection and render each into a separate component. Confirm they show the same documents for the same user.
- Open the browser's network panel and inspect the WebSocket frames. A subscription should produce added/changed/removed DDP messages; a Method call should produce a single result message. This is the clearest way to see which path is doing the work.
- Sign in as two accounts with different
ownerIdvalues. Both paths should return only the caller's documents. If either leaks, the filter is wrong, not the client. - Vary subscriber count and document set size while watching server CPU and memory. The publication's cost should grow with observers; the Method's should track call volume.
Limitations and what to verify
Observer reuse and oplog tailing behavior depend on deployment topology and package versions, so the scaling figures above are directional rather than measured. Reactive pagination beyond a fixed limit requires a custom join, which is real work and should be budgeted. Client-side checks are never security; both paths must enforce authorization on the server. Before porting Meteor 2 code, read the installed release notes for async API changes, and treat any cursor method name in this example as needing confirmation against your version.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.