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
- REST
- The Push Problem
- Polling and Long Polling
- Server-Sent Events
- WebSockets
- Choosing
- 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 nothingThe 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 reconnectsLong 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 stateThe 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
- Why does
POST /getUserthrow away something valuable even though it works? - Why is there no polling interval that is both responsive and efficient?
- 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