The Real-Time Challenge
Real-time applications power some of the most engaging experiences on the web: live chat, collaborative editing, multiplayer games, stock tickers, and notification systems. Yet AI code generators consistently struggle with real-time architecture, often producing solutions that are fundamentally wrong for the use case.
The 2025 CodeRabbit report reveals that AI-generated code produces approximately 1.7x more issues overall, with concurrency-related problems showing ~2x increases compared to human-written code. Real-time applications compound these challenges because they require understanding concurrent connections, state synchronization, network failures, and protocol-specific behaviors that AI training data often oversimplifies.
The Core Problem: AI tools default to familiar request-response patterns, suggesting polling when WebSockets are appropriate, missing heartbeat implementations, ignoring reconnection logic, and creating architectures that fail catastrophically under real-world network conditions.
Why AI Struggles with Real-Time Architecture
Training Data Bias Toward Request-Response
The vast majority of web development tutorials, Stack Overflow answers, and open-source code follows traditional HTTP request-response patterns. AI models trained on this data naturally default to familiar patterns even when they're suboptimal:
- Polling dominance: Simple setInterval + fetch examples vastly outnumber WebSocket tutorials
- REST-first thinking: Most API examples are RESTful, not event-driven
- Single-connection focus: Training data rarely covers multi-instance scaling scenarios
- Happy-path examples: Tutorials skip network failure handling and reconnection logic
Missing Domain Context
AI lacks understanding of when real-time push is more appropriate than request-response. It doesn't inherently know that:
- A chat application needs instant message delivery (WebSocket), not periodic polling
- A live sports score feed is server-to-client only (SSE is sufficient)
- A collaborative document editor needs conflict resolution and operational transforms
- A notification system needs persistence for offline users
WebSocket vs SSE vs Polling: The Decision Framework
One of AI's biggest failures is suggesting the wrong protocol for the use case. Here's the definitive decision framework:
Does client need to send frequent messages to server?
├── YES → Use WebSocket
│ Examples: Chat, gaming, collaborative editing
│
└── NO → Does client need real-time updates from server?
├── YES → Use SSE
│ Examples: Dashboards, notifications, live feeds
│
└── NO → Use REST with optional polling
Examples: Traditional CRUD, infrequent updates
When AI Gets It Wrong
// AI often generates this for chat applications - WRONG
function ChatComponent() {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Polling every 2 seconds - terrible for chat!
const interval = setInterval(async () => {
const response = await fetch('/api/messages');
const newMessages = await response.json();
setMessages(newMessages);
}, 2000);
return () => clearInterval(interval);
}, []);
// Problems:
// 1. 2 second delay for new messages
// 2. Wasted requests when no new messages
// 3. Server load scales linearly with users
// 4. Battery drain on mobile devices
}
// Proper WebSocket implementation for chat - CORRECT
function ChatComponent() {
const [messages, setMessages] = useState([]);
const wsRef = useRef(null);
const reconnectTimeoutRef = useRef(null);
const connect = useCallback(() => {
const ws = new WebSocket(`wss://${window.location.host}/ws/chat`);
ws.onopen = () => {
console.log('Connected to chat');
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
ws.onclose = (event) => {
if (!event.wasClean) {
reconnectTimeoutRef.current = setTimeout(connect, 3000);
}
};
wsRef.current = ws;
}, []);
useEffect(() => {
connect();
return () => {
wsRef.current?.close();
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
};
}, [connect]);
const sendMessage = (content) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: 'message', content }));
}
};
return /* ... */;
}
Common AI Real-Time Anti-Patterns
Anti-Pattern 1: Missing Heartbeat Implementation
AI rarely implements heartbeat/ping-pong mechanisms, leading to zombie connections that waste resources and fail silently.
// Production: With Heartbeat
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.pingInterval = null;
this.pongTimeout = null;
this.PING_INTERVAL = 25000; // 25 seconds
this.PONG_TIMEOUT = 5000; // 5 seconds to respond
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
this.clearPongTimeout();
return;
}
this.handleMessage(data);
};
this.ws.onclose = (event) => {
this.stopHeartbeat();
if (!event.wasClean) {
this.reconnect();
}
};
}
startHeartbeat() {
this.pingInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
this.pongTimeout = setTimeout(() => {
console.warn('Pong timeout - connection may be dead');
this.ws.close();
}, this.PONG_TIMEOUT);
}
}, this.PING_INTERVAL);
}
}
Anti-Pattern 2: No Reconnection with Backoff
// Production: Exponential Backoff
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.maxRetries = options.maxRetries || 10;
this.baseDelay = options.baseDelay || 1000;
this.maxDelay = options.maxDelay || 30000;
this.retryCount = 0;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.retryCount = 0; // Reset on successful connection
};
this.ws.onclose = (event) => {
if (!event.wasClean && this.retryCount < this.maxRetries) {
const delay = this.calculateBackoff();
console.log(`Reconnecting in ${delay}ms (attempt ${this.retryCount + 1})`);
setTimeout(() => this.connect(), delay);
this.retryCount++;
}
};
}
calculateBackoff() {
// Exponential backoff with jitter
const exponentialDelay = this.baseDelay * Math.pow(2, this.retryCount);
const jitter = Math.random() * 1000;
return Math.min(exponentialDelay + jitter, this.maxDelay);
}
}
Scaling with Redis Pub/Sub
A single WebSocket server works fine for small applications, but scaling requires Redis as a message broker between server instances.
// Socket.IO with Redis Adapter
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
async function createScalableServer(httpServer) {
const io = new Server(httpServer, {
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
methods: ['GET', 'POST']
},
pingTimeout: 20000,
pingInterval: 25000,
transports: ['websocket', 'polling']
});
// Create Redis clients for pub/sub
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
// Use Redis adapter - messages now sync across all server instances
io.adapter(createAdapter(pubClient, subClient));
return io;
}
Implementing Presence Systems
Presence systems (online/offline status, typing indicators, active users) require careful coordination between connections and state. AI typically generates naive implementations that fail at scale.
// Production: Redis-Backed Presence
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
class PresenceManager {
constructor(io) {
this.io = io;
this.PRESENCE_KEY = 'presence:online';
this.PRESENCE_TTL = 60; // seconds
this.HEARTBEAT_INTERVAL = 30000; // 30 seconds
}
async setOnline(userId, metadata = {}) {
const presenceData = JSON.stringify({
...metadata,
lastSeen: Date.now()
});
await redis.zadd(this.PRESENCE_KEY, Date.now(), `${userId}:${presenceData}`);
await redis.publish('presence:change', JSON.stringify({
type: 'online',
userId,
metadata
}));
}
async getOnlineUsers() {
const cutoff = Date.now() - (this.PRESENCE_TTL * 1000);
const members = await redis.zrangebyscore(this.PRESENCE_KEY, cutoff, '+inf');
return members.map(member => {
const [userId, ...rest] = member.split(':');
const metadata = JSON.parse(rest.join(':'));
return { userId, ...metadata };
});
}
}
Testing Race Conditions
Real-time applications are prone to race conditions that are difficult to reproduce and test. AI-generated tests rarely cover concurrent scenarios.
// Jest tests for race conditions
describe('Real-time message ordering', () => {
it('should maintain message order under concurrent sends', async () => {
const messageCount = 100;
const receivedMessages = [];
// Connect receiver and multiple senders
const receiver = io(`http://localhost:${server.port}`);
receiver.on('message', (msg) => receivedMessages.push(msg));
const senders = await Promise.all(
Array.from({ length: 5 }, async (_, i) => {
const client = io(`http://localhost:${server.port}`);
await new Promise(resolve => client.on('connect', resolve));
return { client, senderId: i };
})
);
// Send messages concurrently
await Promise.all(
senders.flatMap(({ client, senderId }) =>
Array.from({ length: messageCount / 5 }, (_, i) => {
return new Promise(resolve => {
client.emit('message', { senderId, sequence: i });
setTimeout(resolve, Math.random() * 10);
});
})
)
);
// Verify per-sender ordering
senders.forEach(({ senderId }) => {
const senderMessages = receivedMessages
.filter(m => m.senderId === senderId)
.map(m => m.sequence);
expect(senderMessages).toEqual([...senderMessages].sort((a, b) => a - b));
});
});
});
Key Takeaways
Real-Time Architecture Essentials
- Choose the right protocol: Use WebSocket for bidirectional communication, SSE for server-to-client streaming, and avoid polling for real-time features
- Implement heartbeats: 20-30 second ping intervals detect dead connections before they cause problems
- Use exponential backoff: Prevent DDOS-ing your own server with aggressive reconnection attempts
- Scale with Redis: Use Redis Pub/Sub as a message broker between server instances with Socket.IO's Redis adapter
- Persist presence in Redis: In-memory presence state breaks with multiple server instances
- Queue messages during disconnection: Users expect to receive messages sent while they were offline
- Test race conditions: Concurrent scenarios rarely appear in AI training data but break production systems
- Be explicit in prompts: Specify protocol requirements, scaling needs, and error handling expectations
Conclusion
Real-time applications require fundamentally different thinking than traditional request-response architectures. AI code generators, trained predominantly on REST-based examples, consistently fail to produce production-ready real-time code without explicit guidance.
The key to success is understanding when to use WebSockets vs SSE vs polling, implementing proper heartbeat mechanisms, handling reconnection with exponential backoff, and scaling with Redis Pub/Sub. Always test for race conditions and concurrent scenarios that AI training data rarely covers.
When prompting AI for real-time features, be explicit about your protocol requirements, scaling needs, and error handling expectations. Don't accept polling-based solutions for features that require instant updates.
In our next article, we'll explore Multi-Tenant Architecture Oversights: AI's Single-Tenant Default Thinking, examining why AI generates code without tenant isolation and how to implement proper multi-tenancy patterns.