How to Design Pub/Sub Channel Granularity
Channel granularity is the biggest pub/sub design decision. Too coarse and you leak data and waste bandwidth; too fine and you drown in subscriptions.
The single decision that determines whether a pub/sub system is fast, secure, and affordable is channel granularity: how you slice your data into channels that clients subscribe to. Get it too coarse and every client receives events it does not care about, wasting bandwidth and leaking data across boundaries. Get it too fine and clients open thousands of subscriptions, your fan-out logic buckles, and connection overhead eats you alive. The right answer is to make a channel match the smallest unit a client actually needs to watch, and to align that unit with your authorization boundary. Everything else in a real-time backend is downstream of getting this right.
What a channel should map to
A channel is a named stream that clients subscribe to and messages get published to. The design question is: what does one channel represent? The answer should be the natural unit of the thing users watch. In a chat app, that is one conversation. In a project tool, that is one project or one board. In a live dashboard, that is one account's metrics. Pick the noun that a user is looking at on their screen, and make that the channel.
The reason this works is that subscription follows attention. A user viewing a conversation subscribes to that conversation's channel and nothing else. When events fire, only the people actually looking at that conversation receive them. No filtering on the client, no wasted delivery, no accidental exposure of data from a room the user is not in. The channel boundary does the work.
Why too coarse is a security and cost problem
The tempting shortcut is one big channel: everything for an app, or everything for a workspace, published to a single stream, with clients filtering out what they do not need. It is less code up front. It is also a slow-motion disaster.
Coarse channels leak. If a workspace shares one channel, every message for every project reaches every connected member, and the only thing stopping a curious user from reading events they should not see is client-side filtering, which is no security at all. Anything the client receives, the user can inspect. Authorization has to happen at subscription time, per channel, which is only possible if the channel maps to a real permission boundary. I make the full case for this in authorize every real-time subscription.
Coarse channels also waste. Every client pays to receive and discard events meant for others. In a busy workspace that is a constant background flood, and it scales with total activity rather than with what each user is watching. That is exactly the pattern that shows up in real-time backend mistakes at scale: cost that grows with the wrong dimension.
Why too fine is a scaling problem
Overcorrect and you get channels for every tiny thing: one per message, one per field, one per row. Now a single screen requires a client to subscribe to hundreds of channels, each with its own bookkeeping. Subscription and unsubscription churn as the user scrolls becomes its own load. Your server tracks an explosion of tiny streams. The overhead of managing subscriptions starts to cost more than the data you are moving.
The balance is to make the channel the unit of shared interest, not the unit of data. A conversation channel carries every message in that conversation. A board channel carries every card update on that board. Multiple related changes flow through one appropriately scoped channel, so a client subscribes to a handful of channels for a given screen, not a thousand.
Practical rules that hold up
A few rules I follow every time. One, a channel equals an authorization boundary, so subscribing can be checked with a single permission decision. Two, a channel equals the unit a user watches, so subscription follows what is on screen. Three, keep the count of channels a single client subscribes to small, a handful per view, not hundreds. Four, name channels hierarchically so scoping is obvious and predictable, like account then project then thread.
When you have the granularity right, the rest of the system gets easier. Fan-out is bounded because each channel reaches only its subscribers. Authorization is clean because the channel is the permission unit. And scaling behaves, because load tracks real attention instead of total activity. This is one of the primitives I want the platform to handle well rather than reinvent, which is why I build real-time features on AltoHost and spend my design time on granularity rather than on writing a subscription router from scratch. If you are still deciding whether to build or buy that layer, I worked through it in build vs buy real-time infrastructure.
Channel design is not glamorous, but it is the load-bearing wall. Decide it deliberately, tie it to your permission model, and match it to what users actually watch. Do that and the hard problems downstream mostly solve themselves.