Design a Real-Time Chat System - System Design
Learn how to design a scalable real-time chat system that can handle millions of concurrent users with WebSockets, message queuing, and distributed architecture.
Aryansh Kurmi
Software Developer
Design a Real-Time Chat System - System Design
Real-time chat systems are complex distributed systems that require careful consideration of scalability, real-time communication, and data consistency. This post covers the design of a modern chat system like WhatsApp or Slack.
Problem Statement
Design a real-time chat system that can:
- Support millions of concurrent users
- Deliver messages in real-time
- Handle group chats and direct messages
- Provide message history and search
- Work across multiple devices
- Ensure message delivery and ordering
Functional Requirements
- Messaging: Send/receive text, images, files
- Real-time Delivery: Messages delivered instantly
- Group Chats: Support multiple participants
- Message History: Store and retrieve chat history
- User Status: Online/offline status
- Push Notifications: Notify users of new messages
- Message Search: Search through message history
- File Sharing: Support image and file uploads
Non-Functional Requirements
- Scalability: Handle millions of concurrent users
- Low Latency: Messages delivered in <100ms
- High Availability: 99.9% uptime
- Consistency: Message ordering and delivery
- Security: End-to-end encryption
Capacity Estimation
Traffic Estimates
- Active Users: 50 million daily
- Concurrent Users: 5 million
- Messages per Day: 1 billion
- Average Message Size: 100 bytes
- Peak QPS: 100,000 messages/second
Storage Estimates
- Daily Messages: 1 billion × 100 bytes = 100GB
- Annual Storage: ~36TB
- User Data: 50M users × 1KB = 50GB
- Media Files: 10TB (estimated)
High-Level Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Load Balancer │ │ API Gateway │ │ Chat Service │
│ │────│ │────│ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ WebSocket │ │ Message │ │ Database │
│ Service │ │ Queue │ │ Cluster │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Push │ │ File Storage │ │ Search │
│ Service │ │ (S3/CDN) │ │ Service │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Detailed Component Design
1. WebSocket Connection Management
import asyncio
import websockets
import json
from typing import Dict, Set
class WebSocketManager:
def __init__(self):
self.connections: Dict[str, websockets.WebSocketServerProtocol] = {}
self.user_rooms: Dict[str, Set[str]] = {} # user_id -> set of room_ids
self.room_users: Dict[str, Set[str]] = {} # room_id -> set of user_ids
async def handle_connection(self, websocket, path):
user_id = self.authenticate_user(websocket)
if not user_id:
await websocket.close()
return
self.connections[user_id] = websocket
try:
async for message in websocket:
await self.handle_message(user_id, message)
except websockets.exceptions.ConnectionClosed:
await self.handle_disconnection(user_id)
async def handle_message(self, user_id: str, message: str):
data = json.loads(message)
message_type = data.get('type')
if message_type == 'join_room':
await self.join_room(user_id, data['room_id'])
elif message_type == 'send_message':
await self.send_message(user_id, data)
elif message_type == 'typing':
await self.handle_typing(user_id, data)
async def join_room(self, user_id: str, room_id: str):
if user_id not in self.user_rooms:
self.user_rooms[user_id] = set()
self.user_rooms[user_id].add(room_id)
if room_id not in self.room_users:
self.room_users[room_id] = set()
self.room_users[room_id].add(user_id)
# Notify other users in the room
await self.broadcast_to_room(room_id, {
'type': 'user_joined',
'user_id': user_id,
'room_id': room_id
})
async def send_message(self, sender_id: str, message_data: dict):
room_id = message_data['room_id']
message = message_data['message']
# Store message in database
message_id = await self.store_message(sender_id, room_id, message)
# Broadcast to all users in the room
await self.broadcast_to_room(room_id, {
'type': 'new_message',
'message_id': message_id,
'sender_id': sender_id,
'room_id': room_id,
'message': message,
'timestamp': message_data.get('timestamp')
})
async def broadcast_to_room(self, room_id: str, data: dict):
if room_id in self.room_users:
for user_id in self.room_users[room_id]:
if user_id in self.connections:
try:
await self.connections[user_id].send(json.dumps(data))
except websockets.exceptions.ConnectionClosed:
await self.handle_disconnection(user_id)
async def handle_disconnection(self, user_id: str):
if user_id in self.connections:
del self.connections[user_id]
# Remove user from all rooms
if user_id in self.user_rooms:
for room_id in self.user_rooms[user_id]:
if room_id in self.room_users:
self.room_users[room_id].discard(user_id)
del self.user_rooms[user_id]
2. Message Storage and Retrieval
class MessageService:
def __init__(self):
self.db = Database()
self.cache = Cache()
self.message_queue = MessageQueue()
async def store_message(self, sender_id: str, room_id: str, message: str, message_type: str = 'text'):
message_data = {
'id': self.generate_message_id(),
'sender_id': sender_id,
'room_id': room_id,
'message': message,
'message_type': message_type,
'timestamp': datetime.utcnow(),
'status': 'sent'
}
# Store in database
await self.db.insert_message(message_data)
# Cache recent messages
await self.cache.add_to_recent_messages(room_id, message_data)
# Queue for processing
await self.message_queue.enqueue('message_processing', message_data)
return message_data['id']
async def get_message_history(self, room_id: str, limit: int = 50, offset: int = 0):
# Try cache first
cached_messages = await self.cache.get_recent_messages(room_id, limit)
if cached_messages:
return cached_messages
# Get from database
messages = await self.db.get_messages(room_id, limit, offset)
# Cache the results
await self.cache.set_recent_messages(room_id, messages)
return messages
async def search_messages(self, user_id: str, query: str, room_id: str = None):
# Use search service for complex queries
search_results = await self.search_service.search_messages(
user_id=user_id,
query=query,
room_id=room_id
)
return search_results
3. Database Schema
-- Users table
CREATE TABLE users (
id VARCHAR(36) PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
display_name VARCHAR(100),
avatar_url VARCHAR(255),
status ENUM('online', 'offline', 'away', 'busy') DEFAULT 'offline',
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_status (status)
);
-- Rooms table
CREATE TABLE rooms (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
room_type ENUM('direct', 'group', 'channel') NOT NULL,
created_by VARCHAR(36) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_room_type (room_type),
INDEX idx_created_by (created_by)
);
-- Room participants
CREATE TABLE room_participants (
id VARCHAR(36) PRIMARY KEY,
room_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
role ENUM('admin', 'member', 'moderator') DEFAULT 'member',
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_room_user (room_id, user_id),
INDEX idx_room_id (room_id),
INDEX idx_user_id (user_id),
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Messages table
CREATE TABLE messages (
id VARCHAR(36) PRIMARY KEY,
room_id VARCHAR(36) NOT NULL,
sender_id VARCHAR(36) NOT NULL,
message TEXT NOT NULL,
message_type ENUM('text', 'image', 'file', 'system') DEFAULT 'text',
reply_to VARCHAR(36),
edited_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_room_id (room_id),
INDEX idx_sender_id (sender_id),
INDEX idx_created_at (created_at),
INDEX idx_room_created (room_id, created_at),
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (reply_to) REFERENCES messages(id) ON DELETE SET NULL
);
-- Message status (delivery receipts)
CREATE TABLE message_status (
id VARCHAR(36) PRIMARY KEY,
message_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
status ENUM('sent', 'delivered', 'read') DEFAULT 'sent',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_message_user (message_id, user_id),
INDEX idx_message_id (message_id),
INDEX idx_user_id (user_id),
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
4. Message Queue System
import asyncio
from asyncio import Queue
from typing import Dict, List
class MessageQueue:
def __init__(self):
self.queues: Dict[str, Queue] = {}
self.workers: Dict[str, List[asyncio.Task]] = {}
async def enqueue(self, queue_name: str, message: dict):
if queue_name not in self.queues:
self.queues[queue_name] = Queue()
await self.queues[queue_name].put(message)
async def dequeue(self, queue_name: str):
if queue_name not in self.queues:
return None
return await self.queues[queue_name].get()
async def start_worker(self, queue_name: str, worker_func, num_workers: int = 1):
if queue_name not in self.workers:
self.workers[queue_name] = []
for i in range(num_workers):
worker = asyncio.create_task(self._worker(queue_name, worker_func))
self.workers[queue_name].append(worker)
async def _worker(self, queue_name: str, worker_func):
while True:
try:
message = await self.dequeue(queue_name)
if message:
await worker_func(message)
except Exception as e:
print(f"Worker error: {e}")
await asyncio.sleep(1)
# Message processing workers
async def process_message(message_data):
# Update message status
await update_message_status(message_data['id'], 'processing')
# Send push notifications
await send_push_notifications(message_data)
# Update analytics
await update_message_analytics(message_data)
# Update message status
await update_message_status(message_data['id'], 'processed')
async def send_push_notifications(message_data):
room_id = message_data['room_id']
sender_id = message_data['sender_id']
# Get offline users in the room
offline_users = await get_offline_users_in_room(room_id, sender_id)
for user_id in offline_users:
await push_service.send_notification(user_id, {
'title': 'New Message',
'body': message_data['message'][:100],
'room_id': room_id,
'sender_id': sender_id
})
5. Push Notification Service
class PushNotificationService:
def __init__(self):
self.fcm_client = FCMClient()
self.apns_client = APNSClient()
async def send_notification(self, user_id: str, notification: dict):
user_tokens = await self.get_user_tokens(user_id)
for token in user_tokens:
if token.platform == 'android':
await self.fcm_client.send(token.token, notification)
elif token.platform == 'ios':
await self.apns_client.send(token.token, notification)
async def get_user_tokens(self, user_id: str):
# Get all device tokens for the user
return await self.db.get_user_device_tokens(user_id)
async def register_token(self, user_id: str, token: str, platform: str):
await self.db.store_device_token(user_id, token, platform)
async def unregister_token(self, user_id: str, token: str):
await self.db.remove_device_token(user_id, token)
Scalability Solutions
1. Horizontal Scaling with Sharding
class ChatSharding:
def __init__(self, num_shards: int):
self.num_shards = num_shards
self.shards = [ChatService() for _ in range(num_shards)]
def get_shard(self, room_id: str) -> ChatService:
shard_index = hash(room_id) % self.num_shards
return self.shards[shard_index]
async def send_message(self, room_id: str, message: dict):
shard = self.get_shard(room_id)
return await shard.send_message(room_id, message)
async def get_message_history(self, room_id: str, limit: int):
shard = self.get_shard(room_id)
return await shard.get_message_history(room_id, limit)
2. Load Balancing WebSocket Connections
class WebSocketLoadBalancer:
def __init__(self):
self.servers = []
self.server_weights = {}
self.connection_counts = {}
def add_server(self, server_url: str, weight: int = 1):
self.servers.append(server_url)
self.server_weights[server_url] = weight
self.connection_counts[server_url] = 0
def get_best_server(self) -> str:
# Weighted round-robin with connection count consideration
best_server = None
best_score = float('inf')
for server in self.servers:
weight = self.server_weights[server]
connections = self.connection_counts[server]
score = connections / weight
if score < best_score:
best_score = score
best_server = server
self.connection_counts[best_server] += 1
return best_server
def remove_connection(self, server_url: str):
if server_url in self.connection_counts:
self.connection_counts[server_url] = max(0, self.connection_counts[server_url] - 1)
3. Caching Strategy
class ChatCache:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=0)
self.cache_ttl = 3600 # 1 hour
async def cache_recent_messages(self, room_id: str, messages: List[dict]):
key = f"messages:{room_id}"
await self.redis.setex(key, self.cache_ttl, json.dumps(messages))
async def get_recent_messages(self, room_id: str) -> List[dict]:
key = f"messages:{room_id}"
cached = await self.redis.get(key)
if cached:
return json.loads(cached)
return []
async def cache_user_status(self, user_id: str, status: str):
key = f"status:{user_id}"
await self.redis.setex(key, 300, status) # 5 minutes TTL
async def get_user_status(self, user_id: str) -> str:
key = f"status:{user_id}"
return await self.redis.get(key) or "offline"
Security Considerations
1. End-to-End Encryption
from cryptography.fernet import Fernet
import base64
class MessageEncryption:
def __init__(self):
self.key = Fernet.generate_key()
self.cipher = Fernet(self.key)
def encrypt_message(self, message: str, room_key: bytes) -> str:
room_cipher = Fernet(room_key)
encrypted = room_cipher.encrypt(message.encode())
return base64.b64encode(encrypted).decode()
def decrypt_message(self, encrypted_message: str, room_key: bytes) -> str:
room_cipher = Fernet(room_key)
decoded = base64.b64decode(encrypted_message.encode())
decrypted = room_cipher.decrypt(decoded)
return decrypted.decode()
2. Rate Limiting
class RateLimiter:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=1)
async def is_rate_limited(self, user_id: str, action: str, limit: int, window: int) -> bool:
key = f"rate_limit:{user_id}:{action}"
current = await self.redis.incr(key)
if current == 1:
await self.redis.expire(key, window)
return current > limit
Monitoring and Analytics
1. Metrics Collection
class ChatMetrics:
def __init__(self):
self.metrics = {}
def record_message_sent(self, room_id: str, message_type: str):
self.metrics['messages_sent'] = self.metrics.get('messages_sent', 0) + 1
self.metrics[f'messages_sent_{message_type}'] = self.metrics.get(f'messages_sent_{message_type}', 0) + 1
def record_connection(self, user_id: str):
self.metrics['active_connections'] = self.metrics.get('active_connections', 0) + 1
def record_disconnection(self, user_id: str):
self.metrics['active_connections'] = max(0, self.metrics.get('active_connections', 0) - 1)
Conclusion
Designing a real-time chat system requires careful consideration of:
- Real-time Communication: WebSockets for instant message delivery
- Scalability: Horizontal scaling and load balancing
- Data Consistency: Message ordering and delivery guarantees
- Performance: Caching and database optimization
- Security: Encryption and rate limiting
- Reliability: Message queuing and error handling
The key is to start with a simple design and gradually add complexity as requirements evolve.
This design provides a solid foundation for a real-time chat system. For production use, additional considerations like message persistence, backup systems, and advanced analytics would be needed.