Apollo 24: Real-Time Architecture
easyโฑ 5 mincourseapollo
The real-time spectrum
Real-time data strategies form a spectrum of complexity vs latency:
- Manual refetch (
refetch() on button click) โ Zero latency guarantee, zero infrastructure cost. Good for user-triggered refreshes.
- Polling (
pollInterval: 5000) โ Predictable latency (max = poll interval). No infrastructure beyond HTTP. Good for dashboards.
- subscribeToMore โ Combines a query (initial load) with a subscription (real-time updates). The subscription appends to the query result. Perfect for feeds.
- useSubscription โ Pure push-based data. Requires WebSocket infrastructure. Best for chat, notifications, live cursors.
- Live queries โ Server pushes updated query results automatically when underlying data changes. Experimental/emerging pattern.
Start simple (polling), upgrade only when latency requirements demand it.
// โโ Strategy 1: Polling for dashboard metrics โโโโโโโโโโ
const { data: metrics } = useQuery(GET_METRICS, {
pollInterval: 10000, // refresh every 10s
});
// โโ Strategy 2: subscribeToMore for feeds โโโโโโโโโโโโโโ
const { data, subscribeToMore } = useQuery(GET_MESSAGES);
useEffect(() => {
return subscribeToMore({
document: ON_NEW_MESSAGE,
updateQuery: (prev, { subscriptionData }) => ({
messages: [...prev.messages, subscriptionData.data.messageAdded],
}),
});
}, [subscribeToMore]);
// โโ Strategy 3: useSubscription for live events โโโโโโโโ
const { data: event } = useSubscription(ON_TYPING, {
variables: { channelId },
});
Combine polling + subscriptions
Build a dashboard with three real-time sections:
- Metrics panel using polling (
pollInterval: 10000) for server stats.
- Activity feed using
subscribeToMore that appends new events to a GET_ACTIVITY query result.
- Live presence indicator using
useSubscription(ON_USER_STATUS) that shows which users are currently online.
Each section should have a visual indicator showing its real-time strategy (polling interval, subscription status).
Scaling WebSockets
WebSocket subscriptions don't scale like HTTP:
- Each connection holds a persistent TCP socket on the server.
- Load balancers need sticky sessions or WebSocket-aware routing.
- With 10K concurrent users, you need 10K open connections.
Mitigation strategies:
- Use polling for most data, subscriptions only for truly real-time features.
- Use server-sent events (SSE) instead of WebSockets โ they work over HTTP/2 and through proxies.
- Consider GraphQL over SSE with libraries like
graphql-sse.
- Use a managed service (Pusher, Ably) for the WebSocket layer and trigger from your GraphQL resolvers.
// GraphQL over Server-Sent Events (SSE)
// Alternative to WebSockets โ works through proxies
import { createClient } from 'graphql-sse';
const sseClient = createClient({
url: '/graphql/stream',
});
// Use with Apollo via a custom link
const sseLink = new ApolloLink((operation) => {
return new Observable(observer => {
const unsubscribe = sseClient.subscribe(
{ query: print(operation.query), variables: operation.variables },
{ next: observer.next.bind(observer), complete: observer.complete.bind(observer), error: observer.error.bind(observer) }
);
return unsubscribe;
});
});
Decision matrix for real-time strategy
Use this decision matrix:
| Requirement | Strategy | Example |
|---|
| Data changes every few minutes | Polling (30-60s) | Dashboard stats |
| Data changes every few seconds | Polling (3-5s) | Stock prices |
| User needs instant feedback | subscribeToMore | Chat messages |
| Sub-second latency required | useSubscription | Live cursors, typing indicators |
| Data changes server-side, client needs push | Live queries | Collaborative editing |
Key rule: never use subscriptions when polling is sufficient. Each subscription is a persistent connection โ each poll is a single HTTP request that closes immediately.