Computer Networks

The Application Layer

REST, WebSockets and Real-Time

REST is an architectural STYLE, not a protocol.

JrCodex·8 min read

Jr Codex Computer Networks Notes

Level: Intermediate Prerequisites: Chapter 3: HTTP/2, HTTP/3 and Performance Time to complete: ~20 minutes


Table of Contents

  1. REST
  2. The Push Problem
  3. Polling and Long Polling
  4. Server-Sent Events
  5. WebSockets
  6. Choosing
  7. Summary & Next Steps

1. REST

The Constraints
─────────────────────────────────────────
  REST is an architectural STYLE, not a protocol.
  Its constraints:

    CLIENT-SERVER      separation of concerns
    STATELESS          each request is
                       self-contained (Chapter 2)
    CACHEABLE          responses declare their
                       cacheability
    UNIFORM INTERFACE  resources, identified by
                       URIs, manipulated with
                       standard methods
    LAYERED            proxies and gateways may sit
                       between transparently
─────────────────────────────────────────
Resource-Oriented URLs
─────────────────────────────────────────
  ✗ VERBS IN THE PATH
    POST /getUser?id=42
    POST /deleteArticle
    GET  /createOrder

  ✓ NOUNS, with the METHOD as the verb
    GET    /users/42
    DELETE /articles/17
    POST   /orders

  The method already expresses the action, and
  expresses it in a way infrastructure understands
  (Chapter 2's safe and idempotent properties).
  Putting the verb in the path throws that away.
─────────────────────────────────────────
A Conventional Resource
─────────────────────────────────────────
  GET    /articles           list
  POST   /articles           create      ──► 201 +
                                              Location
  GET    /articles/42        retrieve
  PUT    /articles/42        replace wholly
  PATCH  /articles/42        update partially
  DELETE /articles/42        remove      ──► 204

  Nested:
  GET    /articles/42/comments
  POST   /articles/42/comments

  Filtering, sorting, paging as QUERY PARAMETERS —
  they modify the view, not the resource:
  GET /articles?author=asha&sort=-created&limit=20
─────────────────────────────────────────
Practical Guidance
─────────────────────────────────────────
  - VERSION the API: /v1/articles, or a header.
    You will need it.
  - PAGINATE every collection. An unbounded list
    endpoint is a future outage.
    Prefer cursor pagination over offset (DBMS
    Notes, Module 3, Chapter 2).
  - USE STATUS CODES HONESTLY. Returning 200 with
    {"error": ...} breaks every retry, cache and
    monitoring layer that reads status codes.
  - RETURN THE CREATED RESOURCE on 201, so the
    client need not fetch it.
─────────────────────────────────────────

2. The Push Problem

HTTP Is Request-Response
─────────────────────────────────────────
  The client asks; the server answers. The server
  cannot speak first.

  That is fine for fetching pages. It is wrong for:

    chat messages
    live prices and scores
    collaborative editing
    notifications
    progress on a long job
    dashboards

  In all of these the SERVER knows something new
  and the CLIENT does not know to ask.
─────────────────────────────────────────
Four Answers
─────────────────────────────────────────
  POLLING        ask repeatedly
  LONG POLLING   ask, and the server holds the
                 request open until it has news
  SSE            one long-lived response the server
                 streams into
  WEBSOCKETS     upgrade to a full-duplex
                 connection
─────────────────────────────────────────

3. Polling and Long Polling

# SHORT POLLING — simple, and wasteful.
import time, requests
 
def poll(url, interval=5):
    last = None
    while True:
        r = requests.get(url, params={"since": last})
        if r.json()["events"]:
            handle(r.json()["events"])
            last = r.json()["cursor"]
        time.sleep(interval)          # most requests return nothing
The Arithmetic of Polling
─────────────────────────────────────────
  10,000 clients polling every 5 seconds =
  2,000 requests per second, whether or not
  anything changed.

  Each carries full HTTP headers (Chapter 3's
  compression helps, but they are not free).

  And the average latency of a new event is HALF
  THE INTERVAL — 2.5 seconds. Shortening the
  interval improves latency and multiplies load
  linearly.

  There is no setting that is both responsive and
  efficient. That is the flaw.
─────────────────────────────────────────
# LONG POLLING — the server holds the request open.
def long_poll_handler(request, timeout=30):
    """Server side: wait for an event, or return empty at the timeout."""
    event = event_queue.wait(since=request.cursor, timeout=timeout)
    if event:
        return 200, {"events": [event], "cursor": event.id}
    return 200, {"events": [], "cursor": request.cursor}     # client reconnects
Long Polling, Honestly
─────────────────────────────────────────
  + near-instant delivery
  + works through every proxy and firewall, because
    it is ordinary HTTP
  - one held connection PER CLIENT, occupying
    server resources
  - a reconnect after every message, so a chatty
    stream degenerates toward polling
  - proxies with request timeouts may cut it

  It was the standard technique before SSE and
  WebSockets, and remains a reasonable fallback.
─────────────────────────────────────────

4. Server-Sent Events

The Idea
─────────────────────────────────────────
  ONE HTTP response that never ends. The server
  writes events into it as they occur.

  It is ordinary HTTP with
  Content-Type: text/event-stream, so proxies,
  compression and HTTP/2 multiplexing all work
  normally.

  ONE DIRECTION ONLY: server to client.
─────────────────────────────────────────
The Wire Format
─────────────────────────────────────────
  HTTP/1.1 200 OK
  Content-Type: text/event-stream
  Cache-Control: no-cache

  id: 1
  event: price
  data: {"symbol":"ACME","price":41.20}

  id: 2
  event: price
  data: {"symbol":"ACME","price":41.35}

  Fields are line-based; a BLANK LINE ends each
  event.
─────────────────────────────────────────
def sse_stream():
    """Flask-style generator. Note the heartbeat."""
    yield "retry: 3000\n\n"                      # tell the client how long to wait
    while True:
        event = queue.get(timeout=15)
        if event is None:
            yield ": keepalive\n\n"              # a COMMENT — holds proxies open
            continue
        yield f"id: {event.id}\nevent: {event.type}\ndata: {json.dumps(event.data)}\n\n"
const es = new EventSource("/stream");
es.addEventListener("price", e => update(JSON.parse(e.data)));
es.onerror = () => { /* the browser reconnects AUTOMATICALLY */ };
Why SSE Is Underused
─────────────────────────────────────────
  It does most of what people reach for WebSockets
  to do, with far less machinery:

    - AUTOMATIC RECONNECTION, built into the
      browser
    - AUTOMATIC RESUMPTION: the browser sends
      Last-Event-ID on reconnect, so the server can
      replay what was missed
    - ordinary HTTP, so authentication, cookies,
      compression and observability just work

  If the client only needs to RECEIVE, SSE is
  usually the right answer and WebSockets is
  over-engineering.
─────────────────────────────────────────

5. WebSockets

The Upgrade Handshake
─────────────────────────────────────────
  CLIENT
    GET /ws HTTP/1.1
    Host: example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13

  SERVER
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

  After 101, it is NO LONGER HTTP. The same TCP
  connection now carries WebSocket frames in both
  directions.
─────────────────────────────────────────
What You Get, and What You Lose
─────────────────────────────────────────
  GAIN
    FULL DUPLEX — both sides send at any time
    LOW OVERHEAD — 2-14 bytes per frame, versus
    hundreds for HTTP headers
    Text and binary messages, with boundaries
    preserved

  LOSE
    HTTP caching, status codes, and standard
    middleware
    Automatic reconnection — YOU implement it
    Simple request-response semantics — you build
    your own correlation if you need replies
    Some proxies and load balancers need explicit
    configuration
─────────────────────────────────────────
import asyncio, websockets, json
 
async def handler(ws):
    await ws.send(json.dumps({"type": "welcome"}))
    try:
        async for raw in ws:                     # full duplex: receive any time
            msg = json.loads(raw)
            if msg["type"] == "subscribe":
                await subscribe(ws, msg["channel"])
            elif msg["type"] == "ping":
                await ws.send(json.dumps({"type": "pong"}))
    except websockets.ConnectionClosed:
        await cleanup(ws)                        # ALWAYS clean up per-connection state
The Operational Reality
─────────────────────────────────────────
  A WebSocket is STATEFUL and LONG-LIVED, which
  breaks assumptions HTTP let you ignore:

  - a client is pinned to ONE server, so you need
    sticky routing or a shared pub/sub backend
  - deploys disconnect everyone; you need
    reconnection with backoff and jitter
  - each connection holds memory and a file
    descriptor
  - send application-level PINGS (Module 4,
    Chapter 5's keepalive problem) or NAT and
    proxies will silently drop idle connections
  - authenticate at the HANDSHAKE; there is no
    per-message auth afterwards
─────────────────────────────────────────

6. Choosing

Decision Guide
─────────────────────────────────────────
  Client fetches on demand; no push needed
      ──► ordinary REST over HTTP

  Updates every few minutes, and simplicity wins
      ──► POLLING. Genuinely fine, and do not
          apologise for it.

  Server ──► client only, needs to be prompt
  (feeds, notifications, progress, live figures)
      ──► SSE. The under-used correct answer.

  Genuinely BIDIRECTIONAL and frequent
  (chat, collaborative editing, multiplayer,
   trading)
      ──► WEBSOCKETS

  Very low latency, and loss is acceptable
  (voice, video, games)
      ──► WebRTC over UDP (Module 4, Chapter 2)
─────────────────────────────────────────
The Question to Ask
─────────────────────────────────────────
  "Does the CLIENT need to send frequently, or
   only receive?"

  Only receive ──► SSE. Simpler, resilient, and
  free reconnection.

  Both directions, frequently ──► WebSockets, and
  accept the operational cost.

  Most "real-time" features are receive-only, and
  are built on WebSockets out of habit rather than
  requirement.
─────────────────────────────────────────

7. Summary & Next Steps

Key Takeaways

  • REST's uniform interface puts the verb in the HTTP method rather than the path, which is what lets caches, retries and proxies act correctly without understanding your API.
  • Polling has no good setting: latency averages half the interval, and shortening it multiplies load linearly.
  • SSE gives server-to-client streaming over ordinary HTTP with automatic reconnection and resumption, and is the right answer for most receive-only "real-time" features.
  • WebSockets give full duplex at the cost of HTTP's caching, status codes, middleware and automatic reconnection, plus real operational complexity from long-lived stateful connections.

Concept Check

  1. Why does POST /getUser throw away something valuable even though it works?
  2. Why is there no polling interval that is both responsive and efficient?
  3. What does SSE give you for free that you must implement yourself with WebSockets?

Next Chapter

Chapter 5: Email and Other Protocols


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Computer Networks Index