Skip to main content

Command Palette

Search for a command to run...

Designing a Distributed Real-Time Chat System (Backend Deep Dive)

Updated
5 min readView as Markdown
Designing a Distributed Real-Time Chat System (Backend Deep Dive)
P
Developer. Systems Engineer.

Most chat applications look simple from the outside.

Under the hood, they are complex distributed systems that must handle real-time communication, ordering, failures, and scale.

To understand this deeply, I built a distributed real-time chat backend in Go with support for multi-server architecture, reliable delivery, and scalable message distribution.

System Goal

The system is designed to guarantee:

  • Reliable message delivery

  • Real-time communication

  • Horizontal scalability

  • Fault tolerance

Architecture Overview

Client → WebSocket Server → Connection Manager → Redis → PostgreSQL

  • WebSocket server handles real-time connections

  • Connection manager tracks active users

  • Redis handles distribution, presence, and coordination

  • PostgreSQL ensures durability

WebSocket Server

WebSockets were chosen over HTTP polling because they provide full-duplex communication with low latency.

Each connection is modeled as:

  • user ID

  • socket

  • send channel

Instead of writing directly to the socket, messages are pushed into a channel.

This decouples read and write operations, prevents blocking, and isolates slow clients.

Connection Manager

The connection manager maintains:

map[userID][]Connection

A user can have multiple connections across devices.

A read-write mutex ensures thread-safe access while allowing concurrent reads.

Message Flow

Send flow:

Client → WebSocket → Server → Assign sequence → Store in DB → Fanout

Receive flow:

Channel → Write loop → Socket

This separation ensures clean handling of incoming and outgoing data.

Persistence with PostgreSQL

Messages are stored before being sent.

This guarantees:

  • Crash safety

  • No data loss

Stored data includes:

  • Messages

  • Sequence numbers

  • Delivery state

  • Conversation membership

  • Read receipts

Message Ordering

In a distributed system, messages can arrive out of order.

To solve this, each conversation has a sequence number.

Redis is used for atomic increments:

INCR conversation::seq

This guarantees consistent ordering across servers.

Using the database for sequencing would introduce latency and contention.

Idempotency

Retries can cause duplicate messages.

To handle this:

  • Each message includes a client_msg_id

  • The database enforces uniqueness

  • Duplicate inserts are ignored

This ensures messages are stored only once.

Delivery Guarantee

The system uses at-least-once delivery.

Flow:

  1. Server sends message

  2. Client sends acknowledgment

  3. Server marks message as delivered

If acknowledgments are lost, messages are retried.

Exactly-once delivery is avoided due to complexity and cost.

Fanout Optimization

A naive approach sends one message per user.

This results in O(n) operations.

Instead, users are grouped by server and messages are published once per server.

This reduces complexity to O(number of servers), which is far more efficient for large groups.

Redis Usage

Redis is used for:

  • Pub/Sub for message distribution

  • Presence tracking

  • Sequence generation

  • Rate limiting

Its in-memory design and atomic operations make it ideal for distributed coordination.

Presence System

User presence is tracked using:

user_servers: → set of server IDs

TTL and heartbeat mechanisms ensure stale entries are removed automatically.

Group Chat Model

Instead of separate logic for one-to-one and group chats, everything is modeled as a conversation.

This simplifies system design and reduces complexity.

Message History

Messages are fetched using cursor-based pagination:

  • Query by sequence number

  • Ordered results

  • Limited result set

This approach is more efficient and scalable than offset-based pagination.

Read Receipts

Each user maintains a last seen sequence number per conversation.

This avoids storing per-message read states and reduces write overhead.

Typing Indicators

Typing indicators are treated as ephemeral data.

They are not stored and are only transmitted via pub/sub.

Rate Limiting

Rate limiting is implemented using Redis with atomic counters and TTL.

This prevents abuse and ensures fairness across users.

Backpressure Handling

Slow clients can cause buffer buildup.

To prevent this, non-blocking channel writes are used.

If a client cannot keep up, the connection is dropped.

This protects the system from memory exhaustion.

Observability

The system includes:

  • Structured logging

  • Metrics collection

Observability is critical for debugging and monitoring real systems.

Running the System

Requirements:

  • Go

  • PostgreSQL

  • Redis

Start services and run the server:

go run main.go

Multiple instances can be started to simulate a distributed environment.

Testing

  • Connect using a WebSocket client

  • Authenticate user

  • Send messages

  • Verify delivery and ordering

Multiple server instances can be used to test distribution.

Benchmarking

Key metrics:

  • Messages per second

  • Latency (p50, p95, p99)

  • Memory usage

  • Redis throughput

Load testing tools such as k6 or wrk can be used.

Tradeoffs

Several tradeoffs were made:

  • Redis Pub/Sub instead of Kafka for simplicity

  • At-least-once delivery instead of exactly-once

  • Fanout-on-write strategy for lower latency

These decisions prioritize performance and simplicity.

Failure Scenarios

If a server crashes during message delivery:

  • Message is already stored in the database

  • Client reconnects and fetches missed messages

If an acknowledgment is lost:

  • Retry mechanism resends the message

  • Idempotency prevents duplication

If Redis fails:

  • Cross-server communication is affected

  • System partially degrades

Future Improvements

  • Replace Redis Pub/Sub with Kafka

  • Add message encryption

  • Support media storage

  • Implement sharding

  • Add push notifications

Conclusion

Building a distributed chat system reveals the complexity behind real-time applications.

The system must balance performance, reliability, and scalability while handling failures gracefully.

This project focuses not just on making the system work, but on making it work under real-world conditions.