Live Notifications with Redis: WebSockets, Pub/Sub, and Offline Delivery
A live notification system has two different jobs:
- store important notification state so users can recover it after reconnecting; and
- push new updates quickly to clients that are online right now.
Redis is excellent for the second job. Redis Pub/Sub can fan events across application or WebSocket servers with very little coordination. But Pub/Sub is intentionally ephemeral, so it should not be mistaken for a durable notification inbox.
The clean production pattern is usually:
> durable notification record first, live Redis signal second.
Reference architecture
A practical design looks like this:
The database owns notification history and read state. Redis carries a low-latency signal telling whichever gateway holds the user's connection that something changed.
If the live signal is lost, the user can still recover the notification from durable state.
Why Pub/Sub fits the live path
Publishing is simple:
PUBLISH notifications:user:42 \
'{"notification_id":"n_8172","type":"order_ready"}'
WebSocket gateways subscribe to the channels or routing topics relevant to the connections they own.
Redis then fans the event to subscribers that are connected at that moment.
The official Redis Pub/Sub documentation is explicit about the trade-off: Pub/Sub has at-most-once delivery semantics. If a subscriber is offline or loses the message, Redis does not replay it later.
That is not a defect when the durable record exists elsewhere. It keeps Pub/Sub simple and useful as a live transport.
Never make Pub/Sub the only copy of an important notification
This is unsafe:
create notification
-> PUBLISH payload
-> assume user received it
If the user's WebSocket gateway restarts between those steps, the message can disappear from the user's experience.
Prefer:
create durable notification row
-> commit
-> publish notification ID / change signal
-> connected client receives it immediately
-> disconnected client catches up from the API later
The rule also simplifies debugging: the UI can always reconcile against authoritative notification state instead of treating one socket connection as perfect history.
Store notification state separately from the transport
A durable notification record might contain fields such as:
id
user_id
kind
resource_id
created_at
read_at
payload_version
The live Redis message can be much smaller:
{
"notification_id": "n_8172",
"user_id": "42",
"type": "order_ready"
}
The WebSocket server can either forward that event directly or let the client fetch the canonical representation from the API.
Keeping the live payload small has practical benefits:
- less fan-out bandwidth;
- fewer stale duplicated fields;
- easier schema evolution;
- sensitive data can remain behind normal API authorization;
- reconnect logic uses the same durable source as initial page load.
WebSockets solve browser delivery, not cross-server routing
The browser normally maintains a connection to one WebSocket gateway.
MDN's WebSocket documentation describes WebSockets as a two-way interactive client/server session that lets either side exchange messages without HTTP polling.
That works well until you have multiple gateway instances:
user 42 -> gateway B
order service -> gateway A
Gateway A cannot directly write to a socket owned by gateway B.
Redis Pub/Sub gives the servers a shared live message bus:
The application publishes once. The gateway that owns the user's active connection can deliver it.
Do not expose Redis channels directly to clients
A browser should authenticate to your application/WebSocket gateway, not directly to Redis.
The gateway should decide:
- which user the connection represents;
- which tenant/account it belongs to;
- which notification topics it may receive;
- whether the connection is still authorized after session changes;
- how many subscriptions/connections one user may hold.
Treat channel names as routing internals, not authorization boundaries.
A malicious client must not be able to request something like:
SUBSCRIBE notifications:user:someone_else
and bypass normal application authorization.
Reconnect must be a first-class path
Networks fail. Laptops sleep. Mobile apps background connections. Deployments restart WebSocket servers.
A notification UI therefore needs a reconciliation path.
One simple model is:
1. client connects
2. client receives live events
3. connection drops
4. client reconnects
5. client requests notifications after its last durable cursor/time
6. API returns missed records
7. live delivery resumes
Do not try to solve every disconnect by assuming Pub/Sub will buffer messages—it will not.
If you need the transport layer itself to retain and replay events, Redis Streams may be more appropriate between backend components.
When Redis Streams belong in a notification system
Redis Streams provide retained ordered entries, replay, consumer groups, acknowledgments, and pending-message tracking.
They can be useful when the notification pipeline includes durable asynchronous processing such as:
order event
-> notification builder
-> email worker
-> push worker
-> analytics consumer
Each independent backend can use its own consumer group and progress at its own pace.
That is different from the final WebSocket hop.
A useful split is:
| Layer | Good Redis primitive |
|---|---|
| Durable inter-service notification workflow | Streams |
| Live fan-out to connected gateway processes | Pub/Sub |
| User notification history/read state | Primary durable store |
| Short-lived presence/routing hints | Expiring Redis keys |
Do not force one Redis primitive to solve all four concerns.
Presence can be useful—but keep it advisory
Redis can store short-lived connection or presence metadata:
presence:user:42 -> gateway-b
TTL -> refreshed while connection is healthy
This can reduce unnecessary routing work or help determine whether to attempt immediate delivery.
But presence is inherently racy:
presence says online
-> connection drops one millisecond later
So “online” should be a hint, not proof that a durable notification was seen.
Important delivery state still belongs in the notification record or an explicit application acknowledgment model.
Sent, delivered, and read are different states
Notification systems often accidentally collapse these into one status.
They mean different things:
- created: server committed the notification;
- published: live event was sent to the messaging layer;
- socket-delivered: gateway wrote it to an active connection;
- client-acknowledged: client application confirmed receiving it, if your protocol supports that;
- read: user actually marked/viewed the notification according to product semantics.
A successful Redis PUBLISH does not prove the user read the message.
If product behavior depends on read receipts, model them explicitly in application state.
Multiple devices require deliberate fan-out
One user may have:
- a phone;
- a desktop browser;
- a second browser tab;
- multiple active sessions.
Decide whether a notification should go to:
- every active connection;
- only one device;
- all devices except the sender;
- all sessions in the same tenant;
- a specific conversation or resource subscription.
A gateway often maintains local maps such as:
user_id -> set of socket IDs
room_id -> set of socket IDs
Redis coordinates between gateway instances; local in-process indexes handle sockets owned by one process.
Avoid storing every transient socket operation in the primary database unless the product truly needs that history.
Scaling Pub/Sub in Redis Cluster
Standard Pub/Sub in a Redis Cluster propagates published messages across the cluster.
Redis 7.0 introduced Sharded Pub/Sub with SSUBSCRIBE, SUNSUBSCRIBE, and SPUBLISH. The Redis Pub/Sub documentation explains that shard channels are mapped to cluster hash slots so messages are propagated within the relevant shard rather than across every cluster node.
This can reduce cluster-bus fan-out for large Pub/Sub workloads.
Do not adopt sharded Pub/Sub merely because it exists. First measure whether normal Pub/Sub routing is actually a bottleneck and ensure the client libraries you use support the required cluster behavior correctly.
Backpressure matters at the WebSocket edge
A live system can produce updates faster than a client can consume them.
MDN notes that the widely supported browser WebSocket API does not provide built-in backpressure. If data arrives faster than an application processes it, buffering can grow and the client can become unresponsive.
That means gateways should avoid blindly pushing unlimited events to a slow socket.
Useful safeguards include:
- per-connection outbound queue limits;
- dropping/coalescing replaceable signals such as repeated presence updates;
- disconnecting persistently slow clients;
- sending lightweight change events instead of giant state snapshots;
- forcing the client to reconcile from the API after overload.
A real-time notification is valuable because it is timely, not because every intermediate UI state must be delivered forever.
What should be ephemeral?
Good Pub/Sub candidates are signals the client can recover or reconstruct:
- “new notification available”;
- typing indicators;
- presence changes;
- invalidate/refetch hints;
- progress updates where final state is durable;
- transient UI coordination.
Poor Pub/Sub-only candidates are events whose disappearance causes permanent business loss:
- a payment that must be processed;
- an order that must be fulfilled;
- an audit record;
- the only copy of a user notification;
- a workflow step that requires retry.
Use Streams, a job system, or another durable broker/store for those workflows.
Production checklist
Before shipping a Redis-backed live notification system, verify:
- important notifications are committed durably before live fan-out;
- reconnecting clients can fetch missed notifications;
- Pub/Sub is treated as at-most-once transport;
- WebSocket authentication and authorization happen in the application layer;
- clients cannot choose arbitrary Redis channels;
- multiple browser/device connections are handled intentionally;
- presence is treated as advisory, not durable delivery proof;
- created, delivered, acknowledged, and read states are not confused;
- slow-client/backpressure behavior is bounded;
- Redis outages do not erase the notification inbox;
- Streams are used when backend processing requires retained/replayable events;
- cluster Pub/Sub scaling is measured before adding extra complexity.
The main takeaway
Redis works best in a live notification architecture when it is the fast coordination layer, not the only place the notification exists.
Persist important notification state first. Use Pub/Sub to wake up the gateway that owns an online user's WebSocket. Let reconnecting clients reconcile from durable state. Introduce Streams when backend notification processing needs acknowledgment and replay.
That separation gives you fast live updates without pretending an ephemeral socket or Pub/Sub message is durable delivery.

Discussion (0)