Users do not wait. Every 100ms of latency costs engagement.
AI applications have a latency problem. LLM calls take seconds, not milliseconds. But users expect instant feedback. This post covers techniques for building AI applications that feel real-time.
| Component | Typical Latency | Optimization Potential |
|---|
| Network round-trip | 50-200ms | CDN, edge |
| Input processing | 10-50ms | Optimization |
| Model inference | 500-5000ms | Model choice, streaming |
| Output processing | 10-50ms | Optimization |
| Rendering | 10-50ms | Client optimization |
| Latency | User Perception | Acceptable For |
|---|
| Under 100ms | Instant | All interactions |
| 100-300ms | Fast | Most interactions |
| 300-1000ms | Noticeable | Complex operations |
| 1-3s | Slow | Explained operations |
| Over 3s | Frustrating | Background tasks only |
| Model | TTFT | Full Response |
|---|
| GPT-4 | 500-1500ms | 3-15s |
| GPT-3.5 | 200-500ms | 1-5s |
| Claude 3 Opus | 400-1000ms | 2-10s |
| Claude 3 Haiku | 150-300ms | 0.5-2s |
| Local Llama | 50-200ms | 1-10s |
| Approach | Perceived Latency | Actual Latency |
|---|
| Wait for full response | 5000ms | 5000ms |
| Stream tokens | 500ms (TTFT) | 5000ms |
| Perceived improvement | 90% faster | Same |
Diagram
sequenceDiagram
participant User
participant Frontend
participant Backend
participant LLM
User->>Frontend: Send message
Frontend->>Backend: POST /chat
Backend->>LLM: Stream request
loop Token Stream
LLM-->>Backend: Token
Backend-->>Frontend: SSE: token
Frontend-->>User: Render token
end
LLM-->>Backend: [DONE]
Backend-->>Frontend: SSE: complete
Frontend-->>User: Final render
| Layer | Technology | Purpose |
|---|
| API | Server-Sent Events | Token delivery |
| Backend | Async generators | Stream processing |
| Frontend | Event listeners | Token rendering |
| State | Incremental update | UI state |
Typescript
// Server-side streaming endpoint
export async function POST(req: Request) {
const { messages } = await req.json();
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages,
stream: true,
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content || '';
if (text) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}
`));
}
}
controller.enqueue(encoder.encode('data: [DONE]
'));
controller.close();
},
});
return new Response(readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
Typescript
// Client-side streaming consumer
async function streamChat(message: string, onToken: (text: string) => void) {
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ messages: [{ role: 'user', content: message }] }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('
').filter(line => line.startsWith('data: '));
for (const line of lines) {
const data = line.slice(6);
if (data === '[DONE]') return;
const { text } = JSON.parse(data);
onToken(text);
}
}
}
| Pattern | Use Case | Complexity |
|---|
| Direct passthrough | Simple chat | Low |
| Buffer and batch | Smooth rendering | Medium |
| Transform stream | Content processing | Medium |
| Fork stream | Multiple consumers | High |
| Error Type | Detection | Recovery |
|---|
| Connection drop | No data timeout | Reconnect, resume |
| Malformed data | Parse error | Skip, log |
| Rate limit | 429 response | Backoff, retry |
| Content filter | Stream termination | Show partial, notify |
| Scenario | HTTP Streaming | WebSockets |
|---|
| Single response | Preferred | Overkill |
| Conversation | Works | Better |
| Collaborative | Poor | Preferred |
| Bidirectional | Not possible | Required |
Diagram
flowchart TB
subgraph Clients["Clients"]
C1[User 1]
C2[User 2]
C3[User 3]
end
subgraph Server["WebSocket Server"]
CM[Connection Manager]
MR[Message Router]
SS[State Store]
end
subgraph AI["AI Services"]
LLM[LLM API]
Cache[Response Cache]
end
C1 <-->|WS| CM
C2 <-->|WS| CM
C3 <-->|WS| CM
CM --> MR
MR --> SS
MR --> LLM
LLM --> Cache
style CM fill:#14b8a6,color:#fff
style LLM fill:#f59e0b,color:#fff
| Component | Purpose | Implementation |
|---|
| Connection manager | Handle connections | Connection pool |
| Message router | Route messages | Type-based dispatch |
| State sync | Keep state consistent | Event sourcing |
| Heartbeat | Detect disconnects | Ping/pong |
Typescript
// WebSocket server with AI streaming
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
const connections = new Map<string, WebSocket>();
wss.on('connection', (ws, req) => {
const userId = req.headers['x-user-id'] as string;
connections.set(userId, ws);
ws.on('message', async (data) => {
const message = JSON.parse(data.toString());
switch (message.type) {
case 'chat':
await handleChatMessage(ws, message);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
break;
}
});
ws.on('close', () => {
connections.delete(userId);
});
});
async function handleChatMessage(ws: WebSocket, message: any) {
// Send typing indicator
ws.send(JSON.stringify({ type: 'typing', status: true }));
try {
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: message.messages,
stream: true,
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content || '';
if (text) {
ws.send(JSON.stringify({ type: 'token', text }));
}
}
ws.send(JSON.stringify({ type: 'complete' }));
} catch (error) {
ws.send(JSON.stringify({ type: 'error', message: error.message }));
} finally {
ws.send(JSON.stringify({ type: 'typing', status: false }));
}
}
| Type | Direction | Purpose |
|---|
| query | Client to Server | User input |
| token | Server to Client | Streaming response |
| complete | Server to Client | Response done |
| error | Server to Client | Error notification |
| ping/pong | Bidirectional | Keep-alive |
| Challenge | Solution | Trade-off |
|---|
| Connection limits | Horizontal scaling | Complexity |
| State sharing | Redis pub/sub | Latency |
| Load balancing | Sticky sessions | Uneven load |
| Reconnection | Session persistence | Storage |
| Pattern | Description | Risk |
|---|
| Optimistic send | Show sent immediately | Failure handling |
| Optimistic update | Update UI before confirm | Rollback complexity |
| Skeleton loading | Show structure immediately | Expectation management |
| Progressive enhancement | Add details as available | State management |
| State | Display | Actual Status |
|---|
| User types | Input visible | Local |
| User sends | Message in chat | Sending |
| Server receives | Typing indicator | Processing |
| Tokens arrive | Streaming response | Receiving |
| Complete | Full response | Done |
| Failure | User Experience | Implementation |
|---|
| Send failed | Show retry option | Local state + retry |
| Partial response | Show partial + error | Keep tokens received |
| Timeout | Show timeout message | Cancel + option to retry |
| Benefit | Impact | How |
|---|
| Lower latency | -50-150ms | Closer to user |
| Reduced TTFT | -100ms | Edge preprocessing |
| Better availability | 99.9%+ | Distributed |
| Component | Edge? | Reasoning |
|---|
| Request routing | Yes | Low latency |
| Input validation | Yes | Fast rejection |
| Caching | Yes | Instant hits |
| Small model inference | Maybe | If latency critical |
| Large model inference | No | GPU requirements |
| Cache Type | TTL | Hit Rate |
|---|
| Static responses | 1 hour | 5-10% |
| Frequent queries | 5 min | 10-20% |
| Semantic cache | 1 hour | 20-40% |
| User session | Session | 30-50% |
| Opportunity | Speedup | Implementation |
|---|
| Parallel retrieval | 2-5x | Promise.all |
| Speculative execution | 1.5-2x | Race condition |
| Batch API calls | 3-10x | Batching |
| Pre-computation | Instant | Background jobs |
| Technique | Description | Risk |
|---|
| Speculative retrieval | Fetch likely needed data | Wasted compute |
| Speculative generation | Start multiple models | Cost |
| Predictive prefetch | Anticipate next query | Accuracy |
| Approach | Latency | Cost |
|---|
| Sequential | Sum of all | 1x |
| Parallel | Max of all | 1x |
| Improvement | 50-80% | Same |
| Technique | Effect | Implementation |
|---|
| Progress indicators | Patience | Loading states |
| Typing animation | Anticipation | Animated dots |
| Skeleton screens | Continuity | Placeholder UI |
| Chunked delivery | Engagement | Partial updates |
| Type | Best For | Implementation |
|---|
| Spinner | Unknown duration | Animated icon |
| Progress bar | Known duration | Percentage |
| Skeleton | Content loading | Placeholder shapes |
| Typing dots | AI responding | Animated dots |
Content Chunking
| Chunk Type | Delivery | Effect |
|---|
| Token by token | Real-time | Natural |
| Word by word | Batched | Smoother |
| Sentence by sentence | Logical | Coherent |
| Paragraph by paragraph | Semantic | Contextual |
| Metric | Description | Target |
|---|
| TTFT | Time to first token | Under 500ms |
| TTLB | Time to last byte | Under 5s |
| P95 latency | 95th percentile | Under 2x median |
| Error rate | Failed streams | Under 1% |
| Metric | Description | Target |
|---|
| Perceived wait time | User-reported | Under 1s |
| Abandonment rate | Left before complete | Under 5% |
| Retry rate | User retried | Under 3% |
| Satisfaction | Rating | Over 4/5 |
| Panel | Metrics | Alert Threshold |
|---|
| Latency | TTFT, TTLB, P95 | Over 2x baseline |
| Throughput | Requests/sec | Under 50% capacity |
| Errors | Error rate, types | Over 1% |
| Streams | Active, completed | Anomaly |
| Component | Technology |
|---|
| Client | Fetch + ReadableStream |
| Server | Express + SSE |
| LLM | OpenAI streaming API |
| Component | Technology |
|---|
| Client | React + SWR + streaming |
| Edge | Vercel Edge Functions |
| Backend | Node.js + async iterators |
| LLM | Multiple providers + fallback |
| Monitoring | Custom metrics + alerts |
| Component | Technology |
|---|
| Client | React + WebSocket + offline |
| Edge | Global edge network |
| Backend | Distributed, stateful |
| LLM | Multi-region, failover |
| Observability | Full tracing + RUM |
- 1Streaming is mandatory - Users cannot wait 5 seconds staring at nothing. Stream from the first token.
- 2TTFT is the key metric - Time to first token determines perceived speed. Optimize ruthlessly.
- 3Optimistic UI hides latency - Show progress immediately. Update as data arrives.
- 4Edge reduces latency - Every millisecond at the edge is milliseconds saved for users.
- 5Parallel everything - Sequential calls are slow. Parallelize retrieval, processing, and rendering.
- 6Perception matters - A 3-second stream feels faster than a 2-second wait. Design for psychology.
Real-time AI is not about faster models. It is about faster feedback loops. Stream early, stream often, and never leave users wondering if something is happening.