Migrating from Polling to Reactive Subscriptions in Meteor 3.x
Replace client-side polling with Meteor 3.x reactive subscriptions. A concrete React example shows how to publish a filtered cursor, subscribe with useTracker, and verify instant updates via DDP frames.
01 Jan 2026, 04:37 UTC

The Problem with Polling in Meteor Applications
Many Meteor applications still use setInterval to fetch data every few seconds, creating unnecessary server load and introducing latency that defeats the framework's real-time purpose. The practical takeaway: replace client-side polling with Meteor.subscribe() and reactive data sources so updates arrive within milliseconds of a database write.
How Meteor Pushes Data Without Polling
Meteor's publication/subscription system uses MongoDB change streams (available since MongoDB 3.6) to monitor collection changes. When a publication's cursor matches a write operation, the server pushes the minimal diff over the DDP (Distributed Data Protocol) connection. The client's Minimongo cache updates automatically, and any reactive computation—such as a Blaze helper, React useTracker, or Svelte $: block—re-runs immediately.
Worked Example: Replacing a 5-Second Poll with a Subscription
Assume a Tasks collection where multiple users mark items complete. The legacy client polls every 5 seconds:
// Legacy polling pattern (remove this)
let pollHandle = setInterval(async () => {
const res = await fetch('/api/tasks');
const tasks = await res.json();
setTasks(tasks); // React state setter
}, 5000);Replace it with a publication and a reactive subscription:
// server/publications.js
import { Meteor } from 'meteor/meteor';
import { Tasks } from '/imports/api/tasks';
Meteor.publish('tasks.active', function () {
if (!this.userId) return this.ready();
return Tasks.find({ owner: this.userId, completed: false });
});// client/components/TaskList.jsx (React example)
import { useTracker } from 'meteor/react-meteor-data';
import { Meteor } from 'meteor/meteor';
import { Tasks } from '/imports/api/tasks';
export const TaskList = () => {
const tasks = useTracker(() => {
Meteor.subscribe('tasks.active');
return Tasks.find({ completed: false }, { sort: { createdAt: -1 } }).fetch();
}, []);
return (
<ul>
{tasks.map(t => (
<li key={t._id}>{t.title}</li>
))}
</ul>
);
};Where to run: Server file under server/ (auto-loaded), client component in imports/ui/ or client/.
Required permissions: User must be logged in; publication checks this.userId.
Meaningful placeholders: Replace Tasks with your collection, owner with your ownership field.
Expected check: Open two browser tabs logged in as the same user. Mark a task complete in tab A; tab B updates instantly without refresh.
Risk: Forgetting to stop the subscription on unmount leaks a DDP observer. useTracker handles cleanup automatically; with Blaze use this.subscribe() inside onCreated.
Trade-off: Subscription Overhead vs. Polling Simplicity
Each active subscription holds a server-side observer that tails the MongoDB oplog. For thousands of concurrent users watching large cursors, observer memory can exceed polling's CPU cost. Mitigate by:
- Narrowing publication cursors with field projection (
{ fields: { title: 1 } }). - Using
disableOplog: truefor rarely changing collections and falling back to polling only for those. - Implementing pagination with
limitandskipso observers track smaller result sets.
Verification Checklist
- Remove all
setInterval/setTimeoutdata fetches from client code. - Confirm
meteor shellshowsMeteor.server.publish_handlersincludes your publication name. - In browser dev-tools Network tab, filter WS frames: you should see
subandadded/changed/removedDDP messages, not repeated HTTP GETs. - Measure time from MongoDB write to UI update; target < 100 ms on localhost.
Next Step
Audit your codebase for setInterval calls that hit /api/ or Meteor.call for read-only data. Convert each to a named publication and a reactive subscription. The result is lower server load, instant consistency, and code that aligns with Meteor's reactive architecture.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.