REST vs GraphQL vs WebSockets (and SSE)
Tuesday, 10:04: the mobile beta's hotel screen makes 6 API calls in a row and takes 1.9 s on 4G. And Maya wants "rooms left" to update live, which Karim says is easy: just ask the server every second.
Three ways for apps to talk to your backend. Each fits a different kind of conversation, and many real systems use more than one.
- Problem
A hotel platform serves a web app, two mobile apps and partner agencies. Screens need very different slices of data, and guests want to see availability change live when rooms sell out.
- Why the simple system fails
One fixed set of REST endpoints makes the mobile app do 6 round trips per screen or download huge payloads, and polling every second for availability burns servers while still showing stale counts.
- Engineering concept
Pick the interaction style per access pattern: resource-oriented request/response, client-specified queries over a typed schema, or a long-lived channel for server-initiated updates.
- Technology
REST over HTTP with JSON and HTTP caching; GraphQL with a schema, resolvers and DataLoader; Server-Sent Events and WebSockets for push, often behind a pub/sub layer like Redis.
- Trade-offs
REST is simple and cacheable but rigid in shape; GraphQL is flexible for clients but moves complexity, caching and cost control to the server; push channels are real-time but stateful, harder to scale and to load-balance.
The problem
2 min- Maya · Product managerThe app's hotel screen takes 1.9 s on 4G. And guests want "rooms left" to change live.
- Karim · CTOLive is easy: the app asks the server every second.
- You · Engineer at StayHubCan't the server just tell the app when a room sells?
- Lina · Senior engineer, your mentorGood question. Guess first.
The big idea
4 minEvery API style answers two questions: who shapes the answer? and who speaks first?
Maya's two complaints are one of each. The slow screen is a shape problem; the live count is a who-speaks-first problem.
How it works
5 min- 1REST
: one address per thing; the server decides the shape.
/hotels/42returns 48 KB, the screen uses 6 fields, and needs 6 calls in a row (). The CDN can cache each URL. - 2GraphQL
: one address; the client sends a query naming exactly the fields it wants. The server checks it against a schema and runs a resolver per field.
- 3Polling
Polling: repeat a normal request every N seconds. Load = viewers ÷ N, whether anything changed or not.
- 4SSE
: one long HTTP response the server writes into when a room sells. Server → client only; reconnects by itself.
- 5WebSocket
: a two-way channel; either side sends any time. Needed when the page also sends, like chat.
At peak ~3,000 hotel pages are open; a hotel's rooms change ~3 times an hour. Polling every second: 3,000 requests/s, against a normal site peak of 40, over 99% saying "nothing changed". SSE: 3,000 mostly idle connections, only real changes sent.
Watch a decision being made
4 minLina designs the hotel screen and the live count
The app's hotel screen takes 1.9 s on 4G; Maya wants "rooms left" live for ~3,000 open pages. Pick a style for each need, and make it safe.
- 1Count the trips, answer "who shapes it?"
6 calls × ~320 ms ≈ 1.9 s. With one app and a small team, Lina picks one endpoint built for this screen,
GET /app/hotel-screen/42: 1 trip, ~340 ms, ~4 KB.Why: The cause is trips and wasted bytes. A tailored endpoint adds no new technology; GraphQL pays once many screens need different shapes.
- 2Answer "who speaks first?"
Only the server knows when a room sells, and the page only receives: SSE, not polling, not a WebSocket.
Why: A WebSocket's two-way channel would be unused and makes servers harder to run.
- 3Publish after commit
After the booking commits, publish on Redis ; every realtime server with open streams for hotel 42 forwards it.
ts// After COMMIT, never before: a rolled-back booking must not be announced. await db.commit(); await pubsub.publish(`availability:${hotelId}`, { version, roomType: "deluxe", left: 0 });Why: Announce only what's really true in the database.
- 4Plan for lost events
Version each event. On reconnect the browser sends
Last-Event-ID; the server sends the current count first. The booking still re-checks availability in its transaction.Why: Laptops sleep, servers restart. A pushed count is a hint, never the truth.
In real life
3 minGitHub: REST plus public GraphQL
- Has publicly offered GraphQL alongside REST for years
- Deeply connected data; clients need very different slices
- Publicly documents limits on query cost
Worth it when many clients need different slices of a connected graph.
Stripe: resource-style REST
- Documented as REST: one URL per resource
- Clients are other companies' backends wanting a stable contract
Worth it when predictability beats flexibility.
What goes wrong
3 minYou inherit resolver tuning, query-cost limits and harder caching without a shape problem.
Adopt it when many clients need differently shaped data.
One nested query (hotels → rooms → bookings → guest…) can read millions of rows.
Depth and cost limits, pagination caps, allow-listed queries.
Every server becomes stateful: sticky routing, reconnection storms after deploys.
Push only where the server must speak first.
Trade-offs
3 min| Style | Fits | You pay | Main risk |
|---|---|---|---|
| REST | Partner APIs, cacheable reads | Chatty complex screens | Endpoint sprawl |
| GraphQL | Many own clients, varied screens | Caching, cost limits | N+1 and expensive queries |
| SSE | One-way live updates | Long-lived connections | Lost events |
| WebSocket | Chat, collaboration | Stateful servers | Reconnection storms |
- Karim · CTOChat needs WebSockets anyway. Use them for rooms left too: one technology, not two.
- Lina · Senior engineer, your mentorRooms left flows one way. SSE is plain HTTP: our load balancer, logs and auth already work with it.
- Karim · CTOAnd next quarter we build WebSockets anyway. Two systems.
Your job as tech lead
4 minClaude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.
- Which style each consumer gets: partners keep stable REST; the app may get a tailored call per screen; live availability gets push.
- How live is live enough: ~3 changes an hour per hotel doesn't justify polling every second (3,000 req/s at peak).
- What stays the truth when a pushed update is missed: the booking always re-checks availability.
The mobile hotel screen makes 6 REST calls in a row, ~320 ms each on 4G, ~1.9 s total; /hotels/42 returns 48 KB for 6 fields used. Propose one tailored endpoint returning only what the screen shows; say whether GraphQL or a single REST endpoint, with trade-offs. Separately, rooms left should update live: ~3,000 pages open at peak, ~3 changes an hour per hotel. Compare polling every 2 s with SSE in requests per second. Keep the partner API. Plan only.
- The arithmetic: 3,000 pages polling every 2 s is 1,500 req/s, almost all "nothing changed".
- How many database queries one call to the new endpoint makes for 6 rooms. Growing with rooms means N+1.
- After a laptop sleeps, the page fetches current availability on reconnect.
- The partner API moved to GraphQL or WebSockets, breaking their contract.
- WebSockets for an update the page never answers.
- Pushed "rooms left" treated as truth, skipping the check at booking.
Check yourself
2 minNext episode: the app beta lets anyone who guesses a booking number see another guest's booking, and Karim wants to know who is allowed to see what.
Mark it complete to update your skill map.
Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
Technical deep dive
Here's the same hotel screen both ways. REST makes several trips with server-defined shapes; GraphQL makes one trip with a client-defined shape:
# REST: several round trips, server-defined shapes
GET /v1/hotels/42 → 200 { id, name, address, description, amenities[38], photos[40], … }
GET /v1/hotels/42/rooms → 200 [ { id, name, beds, … }, … ]
GET /v1/hotels/42/rates?date=2026-11-03
GET /v1/hotels/42/reviews?limit=2
# GraphQL: one round trip, client-defined shape
POST /graphql
{ "query": "query { hotel(id: 42) { name rooms { name rate(date: \"2026-11-03\") } reviews(last: 2) { rating text } } }" }Lina designs the API for the hotel screen and the live count
The app's hotel screen takes 1.9 s on 4G, and Maya wants "rooms left" to update live for about 3,000 open pages at peak. Pick an API style for each need, and make it safe.
- 1Count the trips
Trace the screen: 6 calls in a row, each about 300 ms on 4G plus 20 ms of work. 6 × 320 ≈ 1.9 s. The first call alone returns 48 KB; the screen uses 6 fields.
Why: The cause picks the style. Here it's the number of trips and wasted bytes, not slow servers.
- 2Answer "who shapes it?"
The screen needs a shape no single REST resource has. Options: a GraphQL query, or one endpoint built for this screen:
GET /app/hotel-screen/42. With one app and a small team, I pick the tailored endpoint. Result: 1 trip, ~340 ms, ~4 KB.Why: Both fix the trips. The tailored endpoint adds no new technology; GraphQL is worth it once many screens need different shapes.
- 3Answer "who speaks first?"
Only the server knows when a room sells, and the page only receives. So: , not polling and not a WebSocket.
Why: Polling costs viewers × frequency (3,000 req/s at 1 s). A WebSocket's two-way channel would be unused and makes servers harder to run.
- 4Wire the push
After the booking transaction commits, the booking service publishes
availability:hotel-42on Redis . Every realtime server with open streams for hotel 42 forwards it.ts// After COMMIT, never before: a rolled-back booking must not be announced. await db.commit(); await pubsub.publish(`availability:${hotelId}`, { version, roomType: "deluxe", left: 0 });Why: Several realtime servers each hold some of the connections. Pub/sub makes sure every one of them hears about the change.
- 5Plan for lost events
Give each event a version id. When a stream reconnects, the browser sends
Last-Event-ID; the server sends the current count first. And the booking still re-checks availability in the database transaction.Why: Connections drop: laptops sleep, servers restart. A pushed count is a hint for the screen, never the truth for a booking.
// GraphQL resolvers with batching: avoid the N+1 query problem.
import DataLoader from "dataloader";
const rateLoader = new DataLoader(async (roomIds: readonly string[]) => {
const rows = await db.query("SELECT room_id, amount FROM rates WHERE room_id = ANY($1) AND date = $2", [roomIds, date]);
return roomIds.map((id) => rows.find((r) => r.room_id === id)?.amount ?? null); // same order as input
});
const resolvers = {
Hotel: { rooms: (hotel) => db.query("SELECT * FROM rooms WHERE hotel_id = $1", [hotel.id]) },
Room: {
// Naive: one query per room → 1 + N queries. Batched: one query for all rooms in this request.
rate: (room) => rateLoader.load(room.id),
},
};| REST | GraphQL | SSE | WebSocket | |
|---|---|---|---|---|
| Who initiates | Client | Client | Server (after client opens) | Either side |
| Response shape | Server-defined | Client-defined | Server-defined events | Your own protocol |
| HTTP/CDN caching | Excellent (per URL) | Hard (POST, one URL) | N/A | N/A |
| Typical pitfalls | Over/under-fetching, chatty clients | N+1 resolvers, expensive queries | Proxy timeouts, lost events | Stateful servers, reconnection storms |
| Good fit | Public/partner APIs, cacheable reads | Many own clients with varied screens | Live counters, notifications | Chat, collaboration, games |
Interactive simulation
Visual diagram
One hotel platform, three interaction styles. Start the walkthrough.
Analogy
- At hotel receptionFixed forms: room details, bookingTalking to a backendREST endpoints
- At hotel receptionA concierge taking a custom requestTalking to a backendA GraphQL query
- At hotel receptionCalling every minute: "Any rooms free?"Talking to a backendPolling
- At hotel receptionReception calls you when things changeTalking to a backendServer-Sent Events
- At hotel receptionAn open walkie-talkieTalking to a backendWebSockets
Where the analogy breaks: a concierge would refuse to fetch "every review of every hotel". A GraphQL server only refuses if you build those limits in yourself.
Check your understanding
hotels(city: "Lisbon") { name rooms { rate } } for 50 hotels with ~6 rooms each takes 1.8 s. Database logs show ~350 small queries per request. What's going on?Practice ladder
Trace a reconnect
Scenario Warm-upStayHub's hotel page uses SSE for "rooms left". Each event has a version id. The server code: on connect, if the Last-Event-ID header is present, send the current count first; then forward live events.
Timeline for hotel 42 (deluxe rooms): - 20:00 Aiko opens the page. Deluxe left: 3 (version 101). - 20:05 Aiko's laptop goes to sleep. - 20:12 A deluxe room sells (version 102). 20:20 another sells (version 103). - 20:25 Aiko's laptop wakes up and the browser reconnects. - 20:26 Aiko clicks "Book deluxe". At that moment, Kenji is booking the last deluxe room too.
Trace what Aiko's page shows from 20:05 to 20:26, and what protects her booking.
Finish Lina's plan for My trips
Design PracticeMaya: "The app's new My trips screen takes 2.2 s on 4G." Omar's trace: 7 REST calls one after another (bookings, then for each of 3 bookings: hotel and room), about 320 ms each. The screen shows only hotel name, dates, room name and a photo.
Lina's plan has two gaps. Fill in steps 2 and 4.
- Count the trips: 7 × 320 ms ≈ 2.2 s. Most of the bytes returned aren't shown.
- Answer "who shapes it?": ___
- Answer "who speaks first?": the screen doesn't need live data; a normal request is fine.
- Make it safe: ___
- Measure again on 4G.
Payment status on the confirmation page
Design PracticeAfter a guest pays, the confirmation page says "Confirming your payment…". The payment provider usually confirms within 2–20 seconds; StayHub's backend finds out first. Today the page polls GET /bookings/981/status every 500 ms.
About 200 guests are on this page at peak. Pick a style for this page, estimate the load before and after, and say what happens if the guest's connection drops at the wrong moment.
GraphQL for partner hotels?
Decision StretchKarim: "40 partner hotel chains will sync rooms and prices with us next year. One partner asked for GraphQL. Should our partner API be GraphQL or REST?"
The facts: - Partners call from their own backends, in many languages, often nightly. - Hotel content (descriptions, photos) is the same for every partner and changes rarely. - We have 6 engineers and no GraphQL experience. - Our own app may move to GraphQL later.
Write a short decision in this form: "We chose X over Y because…, accepting the risk of…", and name one thing that would change your mind.
Explain it yourself
Your CTO asks: "Should we move everything from REST to GraphQL and WebSockets?" Give a reasoned answer.