WebSocket vs REST vs GraphQL: Which to Choose in 2025
Complete comparison between WebSocket, REST and GraphQL. When to use each technology, pros and cons.
Introduction to Real-Time Communication
Modern applications often need real-time data updates—chat messages, live notifications, collaborative editing, or streaming data. Three main technologies enable this: WebSockets, REST with polling, and GraphQL subscriptions. This guide compares these approaches to help you choose the right one for your use case.
Understanding the Technologies
REST (with Polling)
REST is the traditional request-response model. For real-time updates, clients poll the server at intervals:
// Polling every 5 seconds
setInterval(async () => {
const response = await fetch('/api/messages');
const messages = await response.json();
updateUI(messages);
}, 5000);
WebSockets
WebSockets provide a persistent, bidirectional connection between client and server:
const ws = new WebSocket('wss://api.example.com/chat');
ws.onopen = () => {
console.log('Connected');
ws.send(JSON.stringify({ type: 'subscribe', channel: 'chat' }));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};
ws.onclose = () => {
console.log('Disconnected');
};
GraphQL (with Subscriptions)
GraphQL subscriptions use WebSockets under the hood with a typed query language:
// Apollo Client subscription
const MESSAGES_SUBSCRIPTION = gql`
subscription OnNewMessage($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
content
author {
name
}
}
}
`;
useSubscription(MESSAGES_SUBSCRIPTION, {
variables: { channelId: '123' },
onData: ({ data }) => {
addMessage(data.messageAdded);
}
});
Comparison Matrix
| Feature | REST + Polling | WebSocket | GraphQL |
|---|---|---|---|
| Real-time latency | High (polling interval) | Low | Low |
| Server push | No | Yes | Yes (subscriptions) |
| Bidirectional | No | Yes | Limited |
| HTTP caching | Yes | No | Partial |
| Stateless | Yes | No | Partial |
| Complexity | Low | Medium | High |
| Browser support | Universal | Excellent | Requires library |
When to Use REST
Best For
- CRUD operations: Standard create, read, update, delete
- Infrequent updates: Data that changes every few minutes
- Caching-heavy: Static or semi-static data
- Simple clients: Basic integrations, third-party consumers
- Public APIs: Widely understood, easy to document
REST Polling Patterns
// Short polling - fixed interval
setInterval(() => fetch('/api/data'), 5000);
// Long polling - server holds connection until data
async function longPoll() {
const response = await fetch('/api/data?timeout=30');
handleData(await response.json());
longPoll(); // Immediately reconnect
}
// Exponential backoff for errors
async function pollWithBackoff() {
let delay = 1000;
while (true) {
try {
const data = await fetch('/api/data');
handleData(await data.json());
delay = 1000; // Reset on success
} catch (error) {
delay = Math.min(delay * 2, 30000); // Max 30s
}
await sleep(delay);
}
}
REST Limitations
- Polling wastes bandwidth when no updates
- Delay between updates equals polling interval
- Not suitable for high-frequency updates
- Many connections for multiple resources
When to Use WebSockets
Best For
- Chat applications: Real-time messaging
- Live feeds: Stock prices, sports scores
- Gaming: Multiplayer state synchronization
- Collaborative tools: Real-time document editing
- Notifications: Instant push to clients
WebSocket Server (Node.js)
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
// Track connected clients
const clients = new Set();
wss.on('connection', (ws) => {
clients.add(ws);
console.log('Client connected');
ws.on('message', (data) => {
const message = JSON.parse(data);
handleMessage(ws, message);
});
ws.on('close', () => {
clients.delete(ws);
console.log('Client disconnected');
});
});
// Broadcast to all clients
function broadcast(message) {
const data = JSON.stringify(message);
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
WebSocket Client with Reconnection
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000;
this.handlers = { message: [], open: [], close: [] };
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectDelay = 1000; // Reset
this.handlers.open.forEach(h => h());
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handlers.message.forEach(h => h(data));
};
this.ws.onclose = () => {
this.handlers.close.forEach(h => h());
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
};
}
on(event, handler) {
this.handlers[event].push(handler);
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
}
WebSocket Considerations
- Requires persistent connections (memory/resources)
- Need to handle reconnection logic
- Load balancing is more complex (sticky sessions)
- Firewalls/proxies may interfere
- No built-in request/response pattern
When to Use GraphQL
Best For
- Complex data needs: Multiple related resources
- Mobile apps: Minimize data transfer
- Aggregation: Single query for multiple resources
- Type safety: Strongly typed schema
- Evolving APIs: Add fields without versioning
GraphQL Server Setup
// Apollo Server with subscriptions
import { ApolloServer } from '@apollo/server';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
const typeDefs = `
type Message {
id: ID!
content: String!
author: User!
}
type Query {
messages(channelId: ID!): [Message!]!
}
type Mutation {
sendMessage(channelId: ID!, content: String!): Message!
}
type Subscription {
messageAdded(channelId: ID!): Message!
}
`;
const resolvers = {
Query: {
messages: (_, { channelId }) => getMessages(channelId)
},
Mutation: {
sendMessage: async (_, { channelId, content }) => {
const message = await createMessage(channelId, content);
pubsub.publish('MESSAGE_ADDED', { messageAdded: message });
return message;
}
},
Subscription: {
messageAdded: {
subscribe: (_, { channelId }) =>
pubsub.asyncIterator(['MESSAGE_ADDED'])
}
}
};
GraphQL Trade-offs
- Higher learning curve
- More complex caching
- Potential for expensive queries (N+1)
- Requires client libraries
- Overkill for simple APIs
Hybrid Approaches
REST + WebSocket
Use REST for CRUD, WebSocket for real-time:
// REST for initial data
const messages = await fetch('/api/messages');
// WebSocket for live updates
const ws = new WebSocket('/ws/messages');
ws.onmessage = (event) => {
const newMessage = JSON.parse(event.data);
addMessage(newMessage);
};
GraphQL Queries + Subscriptions
Queries for reads, subscriptions for real-time:
// Initial load with query
const { data } = useQuery(GET_MESSAGES);
// Real-time updates with subscription
useSubscription(MESSAGE_ADDED, {
onData: ({ data }) => {
addToCache(data.messageAdded);
}
});
Performance Considerations
REST
// Reduce payload size
GET /api/messages?fields=id,content
// Use ETags for caching
If-None-Match: "abc123"
// Returns 304 Not Modified if unchanged
WebSocket
// Heartbeat to detect stale connections
setInterval(() => {
ws.send(JSON.stringify({ type: 'ping' }));
}, 30000);
// Compress large messages
ws.send(pako.deflate(JSON.stringify(largeData)));
GraphQL
// Limit query depth
const depthLimit = require('graphql-depth-limit');
// Use DataLoader to batch requests
const userLoader = new DataLoader(ids =>
User.findByIds(ids)
);
Security Considerations
WebSocket Security
// Authenticate on connection
wss.on('connection', (ws, request) => {
const token = request.headers['authorization'];
if (!verifyToken(token)) {
ws.close(4001, 'Unauthorized');
return;
}
});
// Validate all messages
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
if (!isValidMessage(message)) {
throw new Error('Invalid message');
}
handleMessage(message);
} catch (error) {
ws.send(JSON.stringify({ error: 'Invalid request' }));
}
});
Tools and Debugging
For testing and debugging your API implementations:
- JSON Formatter for validating payloads
- Base64 Encoder for token inspection
- Diff Checker for comparing API responses
Decision Guide
Choose REST When
- Updates are infrequent (seconds to minutes)
- Caching is important
- Clients are simple or third-party
- You need broad compatibility
Choose WebSocket When
- Real-time is critical (milliseconds matter)
- Server needs to push to clients
- Bidirectional communication needed
- Building chat, gaming, or live features
Choose GraphQL When
- Clients have varied data needs
- Bandwidth is constrained (mobile)
- Schema evolution is frequent
- Type safety is important
Conclusion
There's no one-size-fits-all solution. REST remains the default for most APIs, WebSockets excel at real-time bidirectional communication, and GraphQL shines for complex client data requirements. Many applications combine these technologies—REST for CRUD, WebSocket for real-time, or GraphQL with subscriptions for a unified approach.
Key takeaways:
- Start with REST unless you have specific real-time needs
- Use WebSocket for true real-time, bidirectional communication
- Consider GraphQL for complex, mobile, or multi-client scenarios
- Hybrid approaches often provide the best of all worlds
For more developer resources, explore our free online tools. For detailed documentation, see MDN WebSocket API and GraphQL documentation.