Call
Redis

Building Scalable Chat with Redis: Messages, Presence, Typing, and WebSocket Fan-Out

A production reference design for Redis-backed chat covering durable messages, Pub/Sub across WebSocket servers, presence TTLs, typing indicators, read cursors, reconnect recovery, multi-device delivery, and hot-room scaling.

Published: December 18, 2024
Updated: September 12, 2026
10 min read
Building Scalable Chat with Redis: Messages, Presence, Typing, and WebSocket Fan-Out

Building Scalable Chat with Redis: Messages, Presence, Typing, and WebSocket Fan-Out

Redis fits chat systems best as a real-time coordination layer: routing messages between WebSocket servers, tracking short-lived presence, carrying typing indicators, and caching hot conversation state.

That does not mean every chat message should exist only in Redis.

For a production chat application, separate two concerns:

  • durable conversation state — messages, membership, read position, moderation/audit data;
  • ephemeral real-time state — active sockets, presence, typing, cross-node fan-out.

Redis is especially strong at the second category.

A practical chat architecture

A multi-node chat backend can look like this:

Rendering diagram…
Diagram generated from the article's Mermaid source.

The normal message path is:

  1. authenticate the sender;
  2. authorize membership in the conversation;
  3. validate and assign a durable message ID/ordering value;
  4. persist the message;
  5. publish a lightweight message.created event;
  6. gateways deliver it to connected members;
  7. disconnected clients catch up from durable history after reconnecting.

The important ordering is persist first, fan out second.

Why Redis Pub/Sub works well between chat servers

A WebSocket connection belongs to one server process. With several gateway instances, the process that receives a new message may not own the recipient's socket.

Redis Pub/Sub gives those processes a shared broadcast layer.

For example:

PUBLISH chat:conversation:8172 \
  '{"type":"message.created","message_id":"m_991"}'

The official Redis Pub/Sub documentation describes exactly this publisher/subscriber model. Redis also lists chat and cross-node WebSocket push as common Pub/Sub use cases.

The application does not need to know which gateway currently owns each recipient connection. Gateways subscribe to the relevant routing channels and deliver events to their local sockets.

Pub/Sub must not be your chat history

Redis Pub/Sub uses at-most-once delivery. If a gateway is disconnected when a message is published, Redis will not replay that Pub/Sub message later.

That makes this design unsafe:

receive chat message
-> PUBLISH full message
-> never store it anywhere durable

A deployment restart or network interruption can create a permanent hole.

Use Pub/Sub as a fast signal:

persist message m_991
-> PUBLISH message.created {id: m_991}
-> online clients see it immediately
-> reconnecting clients fetch messages after their last durable cursor

The durable store—not the WebSocket connection—answers “what messages actually exist?”

The database-to-Pub/Sub gap is a real failure mode

Even “persist then publish” contains two separate operations:

COMMIT message
PUBLISH event

The process can crash after the database commit and before the publish.

The message is safe, but online recipients may not see the live event until they refresh or reconnect.

For many chat products that temporary delay is acceptable because clients reconcile from the database. If immediate fan-out itself must be recoverable, use a more durable bridge such as:

  • a transactional outbox in the primary database;
  • CDC from the committed message/outbox table;
  • a durable Redis Stream or another broker downstream of the durable commit.

Do not claim atomicity across a database and Redis unless you have actually designed it.

Redis Streams when backend consumers need replay

Redis Streams retain ordered entries and support independent consumer groups, acknowledgment, replay, and recovery of pending messages.

They are useful for backend workflows such as:

message committed
-> moderation pipeline
-> push-notification worker
-> search/index worker
-> analytics pipeline

Each consumer group can process the same event stream independently.

That is different from the final WebSocket hop. The browser rarely needs to be a Redis Stream consumer; the application gateway should translate backend events into the client protocol.

A strong design can use Streams for durable backend processing and Pub/Sub for low-latency live fan-out.

Presence should expire automatically

“Online” is not durable truth. Connections disappear without a clean logout because phones sleep, browsers close, networks change, and processes crash.

Redis TTLs are useful for presence leases:

SET presence:user:42 gateway-b EX 45

A healthy connection refreshes the key periodically. If the gateway disappears, the key eventually expires.

The EXPIRE documentation describes Redis key expiration behavior and conditional expiry options.

The exact TTL/heartbeat interval is an application decision. It should balance:

  • how quickly you want stale presence removed;
  • how much heartbeat traffic you accept;
  • mobile/background behavior;
  • temporary network interruptions.

Never interpret an online key as proof that the user is actively looking at the screen. Presence is an approximation.

Typing indicators are deliberately disposable

Typing is almost the perfect Pub/Sub workload.

A typical event:

{
  "type": "typing.started",
  "conversation_id": "8172",
  "user_id": "42"
}

If one typing event is lost, nothing permanent is broken. The UI can clear the indicator after a short local timeout.

Useful rules:

  • rate-limit typing events;
  • send state changes rather than every keystroke;
  • never persist every typing event to the message database;
  • expire typing state automatically;
  • treat it as UI metadata, not business truth.

This is precisely where ephemeral messaging is better than durable queueing.

Keep durable room membership out of transient socket state

Chat systems have several kinds of membership:

user belongs to conversation        -> durable authorization state
socket currently joined to room     -> transient gateway state
user currently online               -> ephemeral presence

Do not confuse them.

A local gateway can keep an in-memory map:

conversation_id -> local socket IDs
user_id         -> local socket IDs

Redis helps coordinate events across gateways, but the durable database should normally answer whether a user is actually allowed to read or send messages in the conversation.

Redis channel names are routing identifiers, not permissions.

Authenticate and authorize before joining real-time channels

Never let a browser connect directly to Redis and choose arbitrary channels.

The WebSocket gateway should authenticate the session and check authorization before accepting operations such as:

join conversation
send message
mark read
load history
subscribe to presence

For each message send, re-check the relevant authorization model where necessary. A user may be removed from a group while an old WebSocket connection is still alive.

Security-sensitive state should not depend solely on “this socket joined the room ten minutes ago.”

Multiple devices change delivery semantics

One user can have several active endpoints:

phone
laptop
browser tab A
browser tab B

Decide explicitly whether an event goes to:

  • all of the user's active connections;
  • all devices except the sending socket;
  • every participant including the sender;
  • only one selected device.

For chat messages, delivering to all active sessions is often useful because it keeps conversation state synchronized across devices. But product rules—not Redis—should define that behavior.

Read state should also be modeled carefully. “Delivered to one browser tab” is not the same as “read by the user on every device.”

Read receipts need durable semantics

A common scalable model stores a read cursor rather than one row per message per user when product semantics allow it:

conversation 8172
user 42
last_read_message_id = m_980

That durable cursor answers which messages are read.

Redis may cache or broadcast read.updated events for fast UI updates, but the final state should survive reconnects.

Keep the state machine explicit:

message persisted
-> delivered to socket (ephemeral transport event)
-> client/user advances read cursor (durable application state)

Do not mark a message “read” merely because PUBLISH returned successfully or because a gateway wrote bytes to a socket.

Ordering comes from the authoritative message model

Users expect conversation messages to appear in a stable order.

Do not build that ordering from client clocks. Devices have different clocks and messages can race through different gateway instances.

Assign ordering on the server side—typically through the durable message store or another deliberately designed sequencing scheme—and include that ordering information in live events.

On reconnect, the client can request messages after its last durable message/cursor and repair any live-delivery gaps.

Redis Pub/Sub preserves the order in which a subscriber receives messages published through Redis, but that alone does not solve business ordering across database commits, concurrent producers, retries, and reconnects.

Hot conversations can become hot channels

A normal private chat distributes traffic naturally across many conversations. A huge public room is different: one message may fan out to many sockets and produce a hotspot.

Plan for:

  • per-room send-rate controls;
  • bounded gateway outbound queues;
  • batching/coalescing non-critical state;
  • separating typing/presence from durable message traffic;
  • horizontally partitioning gateway ownership;
  • measuring per-channel and per-conversation traffic;
  • protecting Redis and gateways from one abusive room.

On Redis Cluster, Sharded Pub/Sub can limit Pub/Sub propagation to the shard owning a shard channel instead of sending every message across the full cluster bus. Redis introduced this model in Redis 7.0.

Use it when measured Pub/Sub cluster traffic justifies the added routing/client complexity—not as a default requirement for small chat systems.

Slow clients need bounded buffers

A recipient on a weak connection may receive messages more slowly than a gateway produces them.

Unbounded per-socket buffers can turn one slow device into a memory problem.

Use limits such as:

  • maximum queued outbound messages/bytes per socket;
  • disconnect/reconnect when the client falls too far behind;
  • drop replaceable typing/presence events first;
  • never silently drop durable chat messages without a reconciliation path;
  • send a “resync required” signal and let the client fetch durable history.

The browser WebSocket API does not provide built-in backpressure in the widely used WebSocket interface, so server and client applications need their own overload behavior.

A reconnect protocol is more important than a perfect socket

Sockets will disconnect. Build recovery into the protocol from day one.

For example:

client reconnects
-> authenticate
-> send last known conversation cursor
-> API returns any missing durable messages
-> restore room subscriptions
-> restore advisory presence
-> resume live Pub/Sub/WebSocket delivery

This design turns live transport into an optimization over durable state instead of a fragile source of truth.

What should Redis own in a chat system?

A useful split is:

ConcernTypical home
Message historyDurable database/store
Conversation membershipDurable database/store
Read cursorDurable database/store
Cross-gateway live eventsRedis Pub/Sub
Durable async event processingRedis Streams or another broker
PresenceRedis TTL keys / ephemeral state
Typing indicatorsPub/Sub / ephemeral state
Local socket membershipGateway memory
Hot counters/cacheRedis where appropriate

Redis can also be used as a primary data store for specific chat designs, but then its persistence, backup, replication, memory, query, and recovery model must meet the same requirements you would demand from any system of record.

Production checklist

Before shipping a Redis-backed chat architecture, verify:

  • messages are durable before live fan-out when durability is required;
  • clients can recover missed messages after reconnect;
  • Pub/Sub loss during disconnect does not create permanent message loss;
  • the database-to-event publication gap has an intentional recovery story;
  • conversation membership is authorized by application state;
  • browsers never connect directly to Redis;
  • presence and typing state expires;
  • multiple devices are handled intentionally;
  • message ordering does not depend on client clocks;
  • read state has explicit durable semantics;
  • slow-socket buffers are bounded;
  • hot conversations cannot monopolize the whole real-time layer;
  • durable backend consumers use Streams/a broker when replay and acknowledgments matter.

The main takeaway

Redis is extremely useful for chat, but the winning architecture is not “put the chat in Redis.”

Use Redis to coordinate the parts that are naturally real time: cross-node WebSocket fan-out, presence, typing, hot state, and event distribution. Keep message history and authorization recoverable from durable application state unless you have deliberately chosen Redis itself as the system of record.

That separation gives you fast chat without making a temporary network disconnect capable of deleting the conversation.

Tags:RedisChatWebSocketsPub/SubReal-TimePresenceMessaging
ENGINEERING & PRODUCT DESIGN

BUILDING A DIGITAL PRODUCT?

Talk to Lofingo about modern web apps, high-performance software engineering, and production digital platforms.

EXPLORE SERVICES →