Skip to content
Episode 19 · Rooms left, live

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.

30 min intermediateREST vs GraphQL vs WebSocketsREST resources & verbsGraphQL: flexible queries vs caching, N+1 and cost controlReal-time push: polling, SSE, WebSocketsAPI as a contractHTTP methods & status codes
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

01

The problem

2 min
Tuesday, 10:04. The rebuilt mobile app is in beta.
  1. Maya · Product manager
    The app's hotel screen takes 1.9 s on 4G. And guests want "rooms left" to change live.
  2. Karim · CTO
    Live is easy: the app asks the server every second.
  3. You · Engineer at StayHub
    Can't the server just tell the app when a room sells?
  4. Lina · Senior engineer, your mentor
    Good question. Guess first.
02

The big idea

4 min
Who shapes it, who speaks first
The big idea

Every 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.

03

How it works

5 min
What really happens in each style
  1. 1
    REST

    : one address per thing; the server decides the shape. /hotels/42 returns 48 KB, the screen uses 6 fields, and needs 6 calls in a row (). The CDN can cache each URL.

  2. 2
    GraphQL

    : 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.

  3. 3
    Polling

    Polling: repeat a normal request every N seconds. Load = viewers ÷ N, whether anything changed or not.

  4. 4
    SSE

    : one long HTTP response the server writes into when a room sells. Server → client only; reconnects by itself.

  5. 5
    WebSocket

    : 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.

04

Watch a decision being made

4 min
Worked example

Lina 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

The whole thing in one line: Tailored endpoint for the shape (1.9 s → 0.34 s); SSE with pub/sub for push (3,000 req/s of polling → a few real events). The database stays the truth.
05

In real life

3 min
Two well-known APIs, two choices

GitHub: 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.

06

What goes wrong

3 min
Choosing GraphQL because it's "modern"

You inherit resolver tuning, query-cost limits and harder caching without a shape problem.

Instead

Adopt it when many clients need differently shaped data.

An unrestricted public GraphQL endpoint

One nested query (hotels → rooms → bookings → guest…) can read millions of rows.

Instead

Depth and cost limits, pagination caps, allow-listed queries.

WebSockets for everything

Every server becomes stateful: sticky routing, reconnection storms after deploys.

Instead

Push only where the server must speak first.

07

Trade-offs

3 min
StyleFitsYou payMain risk
RESTPartner APIs, cacheable readsChatty complex screensEndpoint sprawl
GraphQLMany own clients, varied screensCaching, cost limitsN+1 and expensive queries
SSEOne-way live updatesLong-lived connectionsLost events
WebSocketChat, collaborationStateful serversReconnection storms
Monday, 11:00. Guest–hotel chat is on next quarter's roadmap.
  1. Karim · CTO
    Chat needs WebSockets anyway. Use them for rooms left too: one technology, not two.
  2. Lina · Senior engineer, your mentor
    Rooms left flows one way. SSE is plain HTTP: our load balancer, logs and auth already work with it.
  3. Karim · CTO
    And next quarter we build WebSockets anyway. Two systems.
08

Your job as tech lead

4 min

Claude 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.

Decisions you own
  • 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.
Tell Claude Code
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.
Check before you accept
  • 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.
Red flags
  • 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.
09

Check yourself

2 min
Q1/3Decision
Match each feature to the most natural interaction style:
Partner agencies fetching hotel details, cached at a CDN
Your own mobile app needing varied slices of hotel, rooms, rates and reviews per screen
Showing "rooms left" updating live on a hotel page
Real-time chat between a guest and the hotel
How confident are you?

Next 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.

Finished?

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.
10

Technical deep dive

One trip for the screen, push for the count

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:

http
# 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 } } }" }
REST is predictable and cacheable per URL; GraphQL trades that for a tailored response in one request.
Worked example

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.

  1. 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.

  2. 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.

  3. 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.

  4. 4Wire the push

    After the booking transaction commits, the booking service publishes availability:hotel-42 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: Several realtime servers each hold some of the connections. Pub/sub makes sure every one of them hears about the change.

  5. 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.

The whole thing in one line: Lina split Maya's request into two questions: a tailored endpoint answers "who shapes it" (1.9 s → 0.34 s), and SSE with pub/sub answers "who speaks first" (3,000 req/s of polling → a few real events), with the database as the final truth.
ts
// 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),
  },
};
When StayHub does adopt GraphQL: a DataLoader per request collapses N small queries into one.
RESTGraphQLSSEWebSocket
Who initiatesClientClientServer (after client opens)Either side
Response shapeServer-definedClient-definedServer-defined eventsYour own protocol
HTTP/CDN cachingExcellent (per URL)Hard (POST, one URL)N/AN/A
Typical pitfallsOver/under-fetching, chatty clientsN+1 resolvers, expensive queriesProxy timeouts, lost eventsStateful servers, reconnection storms
Good fitPublic/partner APIs, cacheable readsMany own clients with varied screensLive counters, notificationsChat, collaboration, games
Choose per access pattern. Most real systems use two or three of these.
11

Interactive simulation

12

Visual diagram

Click a component

One hotel platform, three interaction styles. Start the walkthrough.

13

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.

14

Check your understanding

Q1/3Debugging
After launching GraphQL, the query 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?
How confident are you?
15

Practice ladder

Step 1 · Trace

Trace a reconnect

Scenario Warm-up

StayHub'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.

Step 2 · Complete

Finish Lina's plan for My trips

Design Practice

Maya: "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.

  1. Count the trips: 7 × 320 ms ≈ 2.2 s. Most of the bytes returned aren't shown.
  2. Answer "who shapes it?": ___
  3. Answer "who speaks first?": the screen doesn't need live data; a normal request is fine.
  4. Make it safe: ___
  5. Measure again on 4G.
Step 3 · Solve

Payment status on the confirmation page

Design Practice

After 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.

Step 4 · Decide

GraphQL for partner hotels?

Decision Stretch

Karim: "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.

16

Explain it yourself

Your CTO asks: "Should we move everything from REST to GraphQL and WebSockets?" Give a reasoned answer.

0 words