Push Notifications Don't Need a WebSocket
Real-time push notifications rarely need a persistent WebSocket. Learn when native push and fan-out beat holding a socket open, and when a socket actually earns its cost.
Notifications are the feature teams most often over-engineer with real-time infrastructure they do not need. The reflex is: notifications are real-time, real-time means WebSockets, so hold a socket open to every user and push events down it. For most notification products that is wasteful. A user who gets a handful of notifications a day does not justify a persistent connection sitting idle 99.99 percent of the time. Native push and event fan-out deliver the same experience at a fraction of the cost. Reach for a socket only when the interaction is genuinely live and two-way.
The question to ask first
Before you open a socket, ask: does this need to arrive while the app is closed, or only while the user is actively looking at the screen? That single distinction decides the architecture.
If the notification must reach a user who is not in your app, a socket cannot help you: a closed browser tab or a backgrounded mobile app has no live connection. You need native platform push, the Web Push API on web, APNs and FCM on mobile. Those are built exactly for waking a device that is not running your code, and they cost you nothing to keep idle. This is the same instinct as asking whether you actually need WebSockets at all before committing to the operational weight of them.
In-app notifications: fan-out, not a firehose
When the user is inside the app, you do want live delivery of the bell-icon count and the new-item toast. But even here, most notification systems are low-volume per user. The heavy lifting is fan-out: an event happens, and you need to figure out which users care and deliver to the ones currently connected.
Design it as a fan-out over subscriptions. Each user subscribes to a personal notification channel. When an event fires, you resolve the recipient set and publish to those channels. Users who are connected get it live; users who are not get it via native push or see it next time they load. Getting the pub/sub channel granularity right here matters: a per-user channel is clean, a giant shared firehose that every client filters is not.
Keep the payload small and let the client fetch detail if needed. A notification event should carry an id and a type, not the full rendered content, which keeps you sending deltas, not full state and keeps the live path light.
Deliver-once, even across a reconnect
The unforgivable notification bug is the double-buzz: the user gets pinged twice for the same event because a socket dropped and reconnected mid-delivery. Notifications must be idempotent from the user's perspective.
Give every notification a stable id and have the client dedupe on it. On the server, treat delivery as at-least-once and let the id collapse duplicates, the same message delivery guarantee reasoning you would apply to chat. And when a user reconnects, they should get the notifications they missed while offline, which means the notification store is the source of truth and the live socket is just an accelerator. Resume missed messages after a reconnect from that store, do not rely on the socket to have held them.
Rate-limit before you annoy
A real-time notification system will, at some point, try to send a user forty notifications in ten seconds because a batch job fired. That is a product failure, not just a technical one. Batch and throttle on the delivery path: collapse a burst into a digest, cap per-user frequency, and coalesce identical events. Users forgive a slightly delayed notification; they uninstall over a buzzing phone. Apply per-user rate limiting on the real-time path so no single event storm becomes a spam wave.
When a socket actually earns it
There are real-time notification cases where a socket is right: live collaboration mentions, an ops console where seconds matter, a support agent's incoming-chat alert. The tell is that the user is actively present and the notification is part of a live, often two-way, interaction. Then the connection is doing real work every minute, not sitting idle, and the cost is justified.
For everything else, the honest architecture is: native push for reach, a lightweight fan-out for in-app liveness, a durable store for correctness, and throttling for sanity. A platform like AltoHost gives you the in-app fan-out and per-user channels when you need them, while you lean on native push for the closed-app case. If you are unsure which side of the line your feature sits on, evaluate the real-time backend against how often a connection would actually carry traffic, not against the buzzword.
Notifications feel like the obvious place to spend real-time infrastructure. Usually they are the place to save it. Match the transport to whether the user is present, and reach for AltoHost only for the genuinely live path.