When a Meteor app slows down under load, the reflex is to blame the framework. In most of the production apps we look at, the framework is fine — the publications are not. Meteor's pub/sub gives you live queries with almost no code, and that convenience makes it very easy to build subscriptions that scale badly. The good news: pub/sub problems are diagnosable and usually fixable without touching your UI.
How the observer machinery actually costs you
Every Meteor.publish that returns a cursor creates an observer. With oplog tailing enabled (the default when MONGO_OPLOG_URL is set), the server watches MongoDB's replication log and re-evaluates, for every write, which observers care about it. Two properties follow:
- Cost scales with write volume times observer selectivity. A publication like
Orders.find({ userId })is cheap to match against the oplog. One with$where,$oracross fields,limit+sorton a hot collection, or a skip is expensive — some query shapes disqualify oplog tailing entirely and silently fall back to poll-and-diff, re-running the query every 10 seconds per subscriber. - Memory scales with the merge box. The server keeps a per-connection copy of every document it has published, so it can send minimal diffs. Publish 2,000 fat documents to 500 connected clients and you have bought a very large, very busy in-memory cache.
Neither of these is visible in your APM by default, which is why the app "just feels slow."
Measure before you tune
Three sources tell you most of what you need:
- Server metrics per publication. Monti APM (the maintained fork of Kadira) breaks out observer counts, oplog notifications, and response times per publication name. One afternoon of data usually identifies the two publications causing 80 percent of the load.
factsoutput. Thefacts-basepackage exposes live counts of observers, observer handles, and oplog entries followed. A quick check: ifobserve-multiplexersis far belowobserve-handles, observer reuse is working; if they are nearly equal, every subscriber is paying full price.- Mongo's own profiler. Slow publication queries are still slow queries. Missing indexes hurt twice in Meteor: once at initial fetch, then again on every poll-and-diff cycle if the query fell off the oplog path.
The fixes, biggest payoff first
Narrow the fields. fields: { …projection… } on the published cursor shrinks the merge box, the network diff, and client memory in one move. Publishing whole documents "because the client might need them" is the most common single mistake we find.
Make observers reusable. Meteor reuses observer machinery only when two subscriptions run an identical query. Parameterizing publications by user ({ userId: this.userId }) is fine — but gratuitous per-client variation (a timestamp in the selector, a random sort tiebreaker) forces a private observer per client. Normalize the query shape.
Keep hot publications on the oplog path. Avoid skip, keep sort fields indexed and simple, and split $or queries into two publications if that keeps each on the fast path. When you must page, cursor-style pagination (range on an indexed field) beats skip/limit every time.
Stop publishing what should be a method. Live reactivity is worth its cost for the order list on screen. It is not worth it for a settings document read once, a report, or a dashboard refreshed on navigation. Meteor.call (or useTracker around a method result) with an explicit refresh is dramatically cheaper, and users cannot tell the difference for non-collaborative data.
Consider redis-oplog at real scale. Past a few hundred concurrent connections with heavy writes, cultofcoders:redis-oplog changes the economics: writes publish targeted invalidations through Redis instead of every server tailing the whole oplog. It adds an infrastructure dependency and some care around bypassed writes — a deliberate trade, not a default.
A note on the low bar
None of this requires migrating anything. We regularly take Meteor apps from "unusable at 300 users" to comfortable at several times that with projections, two indexes, and demoting three publications to methods — a focused week of work, not a quarter. Pub/sub tuning is also the first thing to do even if you plan to leave Meteor eventually: a calm, measured system migrates far more safely than one on fire.
The pattern behind all of these fixes is the same: reactivity is a budget. Spend it on the data users are actively watching together, and make everything else request/response. Meteor lets you choose per-cursor — most slow apps simply never made the choice.