Protocols Unpacked 0/14

Backend engineeringHow systems talk14 protocols, side by side

Protocols
Unpacked

Every way two programs can talk, from a plain HTTP request to brokers, streams and peer to peer calls. Each protocol gets a one line idea, a plain explanation, the technical detail, a sequence diagram and a working example you can replay.

GraphQLMQTTKafka

The map

Four families cover every protocol here. Start with the shape of the conversation you need, then pick a member.

Ask and answerClientServerClient → Server, then a reply

The client asks, the server replies, and the call is over.

Server tells youClientServerServer → Client, again and again

Updates flow to you without asking for each one.

Talk both waysClientPeerClient ⇄ Peer, all the time

One open connection where either side speaks any time.

Through a brokerProdBrokerC1C2Producer → Broker → Consumer

Senders and receivers never meet; a broker sits in between.

01

HTTP, the road underneath

Most protocols here ride on HTTP, so its three versions decide how fast everything above them feels.

HTTP/1.1HTTP/2HTTP/3Foundation
In one lineRequest and response
In simple words

Like ordering at a counter. You hand over a slip (the request) with a short header and maybe a bag of content, and you get a slip back (the response) with a status number telling you how it went.

Under the hood

HTTP/1.1 sends text messages over TCP and handles one request at a time per connection, so browsers open about six connections per site. HTTP/2 multiplexes many streams over one TCP connection using binary frames and HPACK header compression, but one lost packet stalls every stream (TCP head of line blocking). HTTP/3 runs on QUIC over UDP with TLS 1.3 built in, so streams lose packets independently and connections survive a network change.

Pick it forAlways, underneath. Prefer HTTP/2 or 3 for browsers and APIs with many parallel calls.

Transport
TCP (1.1, 2) · QUIC on UDP (3)
Payload
Headers plus any body
Direction
Client asks, server answers
Ports
80 · 443

How a conversation looks

HTTP/2: two requests share one connection, answers return in any order
Browser
Server
TCP + TLS handshake, once
stream 1 · GET /index.html
stream 3 · GET /app.js
stream 3 · 200 app.js (ready first)
stream 1 · 200 index.html

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
HTTP/1.1curl --http1.1 URLPlain text, supported by every client and proxy, easy to read in a packet captureLegacy clients, quick debugging, simple internal tools
HTTP/2curl --http2 URLMany requests share one TCP connection, with compressed headersThe default for browsers and APIs over TLS; gRPC requires it
HTTP/3curl --http3 URLQUIC avoids TCP head of line blocking and survives a Wi-Fi to mobile switchMobile users, lossy networks, global CDNs
Keep-alive poolnew https.Agent({ keepAlive: true })Reuses warm connections instead of a new TCP and TLS handshake per callAny service calling the same host repeatedly
Conditional GETIf-None-Match: "v7" → 304The server skips the body when nothing changedCaching and cheap change checks

Try it

http.shBASH
curl -sI https://example.com                  # headers only
curl -sI --http2 https://example.com
curl -s -o /dev/null -w "%{http_version}\n" https://example.com
curl -sI --http3 https://cloudflare.com       # needs curl built with HTTP/3

Read the status class first. 2xx worked, 3xx look elsewhere, 4xx the request was wrong, 5xx the server failed. The exact code only matters after that.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -sI --http2 https://example.com
HTTP/2 200
content-type: text/html
cache-control: max-age=3036
curl -s -o /dev/null -w "%{http_version}\n" https://example.com
2

Key terms

TermSimple meaning
MethodThe verb: GET reads, POST creates, PUT replaces, PATCH edits, DELETE removes
Status codeThe outcome as a number, such as 200 or 404
HeaderA labelled detail about the message, like Content-Type
IdempotentRepeating it gives the same result: GET, PUT, DELETE
MultiplexingMany requests in flight on one connection
Head of line blockingOne slow or lost packet holds up everything behind it
02

REST

A style, not a protocol: every resource lives at a URL, and the standard HTTP verbs act on it.

HTTPJSONURLsAsk and answer
In one lineArchitectural style
In simple words

Like a library catalogue. Every book has its own address, /books/42, and the same few verbs work on any book: read it, add one, change it, remove it. Learn the verbs once and every REST API feels familiar.

Under the hood

Defined by Roy Fielding in 2000 as a set of constraints: client and server, stateless requests, cacheable responses, a uniform interface and layered systems. Clients transfer representations of resources, usually JSON. Methods carry the meaning, status codes carry the outcome, ETag and Cache-Control drive caching, and OpenAPI documents the contract. Versioning lives in the URL (/v2) or a header.

Pick it forPublic APIs, CRUD services, anything that third parties or many languages must call.

Transport
HTTP/1.1 or HTTP/2
Payload
JSON, sometimes XML
Direction
Request, then response
Contract
OpenAPI, optional

How a conversation looks

Read a user, then change one field
Client
API
Database
GET /users/42
SELECT … WHERE id = 42
one row
200 OK · {"id":42,"name":"Asha"}
PATCH /users/42 · {"name":"Asha R"}
200 OK · new ETag

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
GETGET /users?page=2&limit=20Safe and cacheable, so CDNs and browsers can reuse answersLists, details, search
POSTPOST /usersCreates a resource and lets the server choose its idNew records, or actions such as POST /orders/7/cancel
PUTPUT /users/42Replaces the whole resource and is idempotent, so retries are safeThe client holds the full object, or upserts by a known id
PATCHPATCH /users/42 {"name":"Asha R"}Sends only what changed, so payloads stay smallEdit forms and partial updates
DELETEDELETE /users/42 → 204Idempotent removal; a second call changes nothingRemoving resources
Idempotency-KeyIdempotency-Key: 8f2c1d…The server stores the first result and replays it for a retried POSTPayments and orders over flaky networks

Try it

rest.shBASH
curl -s https://api.example.com/users/42

curl -si -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Asha","email":"asha@example.com"}'

curl -s -X PATCH https://api.example.com/users/42 \
  -H "Content-Type: application/json" -d '{"name":"Asha R"}'

curl -s -o /dev/null -w "%{http_code}\n" -X DELETE https://api.example.com/users/42

POST answers 201 with a Location. A well behaved API tells you where the new resource lives, so the client never has to guess its URL.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s https://api.example.com/users/42
{"id":42,"name":"Asha","email":"asha@example.com"}
curl -si -X POST https://api.example.com/users -H "Content-Type: application/json" -d '{"name":"Ravi"}'
HTTP/2 201
location: /users/43
content-type: application/json

{"id":43,"name":"Ravi"}
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE https://api.example.com/users/43
204

Key terms

MethodDoesSafeIdempotent
GETRead a resourceYesYes
POSTCreate, or run an actionNoNo
PUTReplace the whole resourceNoYes
PATCHChange some fieldsNoNot by default
DELETERemove itNoYes
03

SOAP

Strict XML envelopes and a machine readable contract, still running banks, telecoms and government systems.

XMLWSDLHTTPAsk and answer
In one lineXML messaging protocol
In simple words

Like sending an official letter in a fixed envelope. Every message has the same envelope, an optional header for stamps and signatures, and a body with the request. A WSDL file is the office rulebook saying which letters it accepts and what it sends back.

Under the hood

SOAP 1.2 (W3C) defines Envelope, Header, Body and Fault. Messages are usually POSTed to one endpoint, with the operation named by SOAPAction (1.1) or the action parameter of application/soap+xml (1.2). WSDL describes operations and XSD types, so clients are generated from it. WS-* standards add message level security (WS-Security signatures and encryption), reliable delivery and transactions. Errors come back as a soap:Fault.

Pick it forIntegrating with enterprise or regulated systems that already publish a WSDL.

Transport
HTTP POST, sometimes SMTP or JMS
Payload
XML envelope
Direction
Request, then response
Contract
WSDL plus XSD, required

How a conversation looks

Fetch the contract once, then exchange envelopes
Client
SOAP service
GET /accounts?wsdl
WSDL: operations and types
generate typed client stubs
POST Envelope · GetBalance
Envelope · GetBalanceResponse
or soap:Fault with a reason

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Document/literalstyle="document" use="literal"The Body is validated against an XSD schema and passes WS-I checksThe default for any new SOAP service
RPC/literalstyle="rpc" use="literal"The Body wraps the operation name around its parametersOlder services designed around method calls
SOAP 1.1Content-Type: text/xml + SOAPActionThe widest tool supportOlder enterprise and government endpoints
SOAP 1.2application/soap+xml; action="…"A W3C standard with a cleaner fault modelNew integrations when both sides support it
WS-Security<wsse:Security> in the HeaderSigns and encrypts the message itself, so it stays protected through intermediariesBanking, healthcare, government
MTOMXOP attachmentsSends binary as raw parts instead of bulky base64 textPDFs, scans and images in SOAP calls

Try it

get-balance.xmlXML
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
               xmlns:acc="http://bank.example.com/accounts">
  <soap:Header>
    <acc:Auth token="eyJhbGciOiJIUzI1NiJ9"/>
  </soap:Header>
  <soap:Body>
    <acc:GetBalance>
      <acc:AccountId>ACC-1042</acc:AccountId>
    </acc:GetBalance>
  </soap:Body>
</soap:Envelope>

The envelope never changes shape. Only the namespaced elements inside Body differ per operation, which is what lets tools validate every message against the WSDL.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s -X POST https://bank.example.com/soap -H 'Content-Type: application/soap+xml; charset=utf-8; action="GetBalance"' --data @get-balance.xml
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Body>
    <acc:GetBalanceResponse xmlns:acc="http://bank.example.com/accounts">
      <acc:Balance currency="INR">48250.00</acc:Balance>
    </acc:GetBalanceResponse>
  </soap:Body>
</soap:Envelope>

Key terms

TermSimple meaning
EnvelopeThe outer wrapper every message must have
HeaderOptional extras: auth, signatures, routing
BodyThe actual request or response
FaultThe standard error format
WSDLThe contract: operations, messages, endpoint
WS-SecuritySigning and encrypting the message itself
04

GraphQL

One endpoint and a typed schema: the client writes a query for exactly the fields it needs, in one round trip.

HTTPJSONschemaAsk and answer
In one lineQuery language for APIs
In simple words

Like a buffet where you fill one plate with exactly what you want, instead of ordering three fixed meals. One request can fetch a user, their last two orders and nothing else.

Under the hood

The server publishes a schema of types plus Query, Mutation and Subscription roots. Clients POST {query, variables} to /graphql, resolvers run per field, and the answer is {data, errors}, often HTTP 200 even with partial errors. It removes over and under fetching, but needs care: batch resolvers with DataLoader to avoid N+1 queries, cap query depth and cost, and use persisted queries since HTTP caching is harder. Subscriptions usually run over WebSocket.

Pick it forFrontends with many screens that each need a different slice of linked data.

Transport
HTTP POST, WebSocket for subscriptions
Payload
Query text in, JSON out
Direction
Request, then response
Contract
Schema, introspectable

How a conversation looks

One query, two services, one answer shaped like the query
Client
GraphQL
Users
Orders
POST /graphql · user { name orders { total } }
resolve user
user 42
resolve orders, batched
2 orders
200 · { data: { user: … } }

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Queryquery { user(id: 42) { name } }Reads exactly the fields a screen needsPages, dashboards, mobile views
Mutationmutation { renameUser(id: 42, name: "A") { id name } }Writes and returns the updated data in one round tripForms and user actions
Subscriptionsubscription { orderUpdated(id: "o_981") { status } }The server pushes changes, usually over WebSocketLive status, chat, notifications
Fragmentfragment UserCard on User { name avatar }Reuses one field set across many queriesComponent based UIs
Variablesquery($id: ID!) + {"id":"42"}Keeps the query text fixed, safe from injection and cacheableAlways, instead of building strings
Persisted query{"extensions":{"persistedQuery":{"sha256Hash":"…"}}}Sends a hash instead of the full text, enabling GET caching and allowlistsPublic production clients and mobile apps

Try it

user-orders.graphqlGRAPHQL
query UserOrders($id: ID!) {
  user(id: $id) {
    name
    orders(last: 2) {
      id
      total
    }
  }
}

The answer mirrors the query. Add or remove a field in the query and the JSON changes to match, without a new endpoint or a new API version.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s https://api.example.com/graphql -H "Content-Type: application/json" -d '{"query":"query($id:ID!){user(id:$id){name orders(last:2){id total}}}","variables":{"id":"42"}}'
{"data":{"user":{"name":"Asha","orders":[{"id":"o_981","total":1499},{"id":"o_990","total":320}]}}}

Key terms

TermSimple meaning
SchemaThe typed menu of everything you can ask for
QueryA read
MutationA write
SubscriptionA live feed of changes
ResolverThe function that fetches one field
N+1 problemOne query per item in a list; fix with batching
05

gRPC

Remote calls defined in .proto files, sent as compact Protocol Buffers over HTTP/2, with streaming in either direction.

HTTP/2ProtobufstreamsAsk and answer
In one lineRemote procedure calls
In simple words

Like calling a function that happens to live on another computer. Both sides share one contract file, and messages travel packed in a small binary form that is quick to send and quick to read.

Under the hood

Services and messages are written in the Protocol Buffers IDL; protoc generates client stubs and server interfaces in each language. Every call is an HTTP/2 stream: POST /package.Service/Method with content-type application/grpc, length prefixed protobuf messages, and the result in trailers (grpc-status). There are four call shapes: unary, server streaming, client streaming and bidirectional, plus deadlines, cancellation and metadata. Browsers cannot read HTTP/2 trailers, so web clients go through gRPC-Web or Connect.

Pick it forInternal service to service calls where speed, strict types and streaming matter.

Transport
HTTP/2
Payload
Protocol Buffers, binary
Direction
Unary or streaming, both ways
Contract
.proto, required

How a conversation looks

A server streaming call: one request, many responses, then a status
Client stub
Server
HEADERS :path /orders.v1.OrderService/Track
DATA TrackRequest { id: o_981 }
DATA TrackUpdate { PACKED }
DATA TrackUpdate { SHIPPED }
TRAILERS grpc-status: 0

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Unaryrpc GetOrder (GetOrderRequest) returns (Order)Plain request and response with strict typesMost calls between services
Server streamingrpc Track (TrackRequest) returns (stream TrackUpdate)Many responses over one call, no pollingProgress, feeds, large result sets
Client streamingrpc Upload (stream Chunk) returns (Summary)The client sends many messages and gets one answerUploads, batched metrics
Bidirectionalrpc Chat (stream Msg) returns (stream Msg)Both sides stream independently on one connectionChat, real time sync
Deadlineclient.getOrder(req, { deadline: Date.now() + 2000 }, cb)Bounds the wait and cancels work down the chainEvery production call
gRPC-Web or ConnectEnvoy proxy or @connectrpc/connect-webBrowsers cannot read HTTP/2 trailers, so they need a bridgeCalling gRPC services from a web app

Try it

orders.protoPROTO
syntax = "proto3";
package orders.v1;

service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc Track (TrackRequest) returns (stream TrackUpdate);
}

message GetOrderRequest { string id = 1; }
message TrackRequest    { string id = 1; }
message Order { string id = 1; int64 total_paise = 2; string status = 3; }
message TrackUpdate { string status = 1; string at = 2; }

Field numbers are forever. The wire format uses the numbers, not the names. Never reuse or renumber a field; add new ones and mark old ones reserved.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
grpcurl -plaintext -d '{"id":"o_981"}' localhost:50051 orders.v1.OrderService/GetOrder
{
  "id": "o_981",
  "totalPaise": "149900",
  "status": "PACKED"
}
grpcurl -plaintext -d '{"id":"o_981"}' localhost:50051 orders.v1.OrderService/Track
{ "status": "PACKED", "at": "2026-10-03T09:10:00Z" }
{ "status": "SHIPPED", "at": "2026-10-03T14:42:00Z" }

Key terms

Call typeShapeExample
UnaryOne request, one responseGetOrder
Server streamingOne request, many responsesTrack a delivery
Client streamingMany requests, one responseUpload sensor readings
BidirectionalBoth sides stream at onceLive chat, game state
06

tRPC

End to end type safety for TypeScript: the client imports the server's types, so there is no schema file and no code generation.

TypeScriptHTTPJSONAsk and answer
In one lineTyped RPC library, not a wire format
In simple words

Like the frontend and backend sharing one dictionary. Rename a field on the server and your editor underlines every place in the frontend that uses it, before anything runs.

Under the hood

You define procedures (query, mutation, subscription) on a router with input validators such as Zod, then export only its type. The client is a typed proxy inferred from that type at compile time. On the wire it is plain HTTP and JSON: queries are GET /trpc/proc?input=…, mutations are POST, httpBatchLink merges calls made in the same tick, and subscriptions run over SSE or WebSocket. Both ends must be TypeScript sharing the types, usually in a monorepo.

Pick it forA TypeScript frontend and backend owned by the same team. Not for public or multi language APIs.

Transport
HTTP, SSE or WebSocket
Payload
JSON
Direction
Request, then response
Contract
TypeScript types, inferred

How a conversation looks

Ordinary HTTP underneath, full types on top
React client
tRPC server
types come from AppRouter at build time, nothing generated
GET /trpc/userById?input={"id":42}
200 · {"result":{"data":{…}}}
POST /trpc/renameUser
200 · {"result":{"data":…}}

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
querytrpc.userById.query({ id: 42 })Sent as GET, so responses can be cachedFetching data
mutationtrpc.renameUser.mutate({ id: 42, name: "A" })Sent as POST for writesCreating and updating
subscription.subscription(async function* () { yield … })Pushes over SSE (httpSubscriptionLink) or WebSocketLive updates
Input validation.input(z.object({ id: z.number() }))Checks data at runtime and infers the TypeScript typeEvery procedure that takes input
Middlewaret.procedure.use(isAuthed)Shares auth, logging and context across proceduresProtected or audited procedures
httpBatchLinklinks: [httpBatchLink({ url })]Merges calls made in the same tick into one requestPages that fire several queries at once

Try it

router.ts + client.tsTS
// server/router.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.create();
export const appRouter = t.router({
  userById: t.procedure
    .input(z.object({ id: z.number() }))
    .query(({ input }) => db.user.findById(input.id)),
});
export type AppRouter = typeof appRouter;

// client.ts
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./server/router";

const trpc = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: "http://localhost:3000/trpc" })],
});
const user = await trpc.userById.query({ id: 42 }); // fully typed

Only the type crosses over. import type is erased at build time, so no server code ships to the browser.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s 'localhost:3000/trpc/userById?input=%7B%22id%22%3A42%7D'
{"result":{"data":{"id":42,"name":"Asha"}}}
# rename name to fullName on the server, then type check the client
npx tsc --noEmit
client.ts(21,18): error TS2339: Property 'name' does not exist on type '{ id: number; fullName: string; }'.

Key terms

TermSimple meaning
RouterThe tree of every procedure the server offers
ProcedureOne callable function: query, mutation or subscription
Input validatorChecks the arguments at runtime, usually Zod
LinkThe client's transport, such as httpBatchLink
BatchingCalls made together travel as one request
07

Polling and long polling

Updates over plain HTTP: ask again on a timer, or ask once and let the server hold the answer until something happens.

HTTPtimerscursorServer tells you
In one lineHTTP workaround for updates
In simple words

Short polling is a child asking "are we there yet?" every five minutes. Long polling is asking once, and the driver only answers when you arrive, or after a while to say "not yet".

Under the hood

Short polling sends requests at a fixed interval, so latency is up to one interval and most answers are empty. Long polling keeps each request open until data arrives or a timeout (often 25 to 30 seconds, below proxy idle limits), then the client asks again at once with a cursor so no event is missed. It works through every proxy and firewall, but each waiting client holds a connection, and synchronized retries can create a thundering herd. RFC 6202 lists the trade offs.

Pick it forJob status checks, or a fallback when SSE and WebSocket are blocked.

Transport
Plain HTTP
Payload
Anything
Direction
Client asks repeatedly
Latency
Interval, or near instant when long

How a conversation looks

Long polling: each answer is followed by the next question
Client
Server
GET /events?after=105
holds the request, up to 30 s
200 · [event 106]
GET /events?after=106
nothing new before the timeout
204 · ask again

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Short pollingsetInterval(() => fetch("/jobs/77"), 5000)The simplest thing that works anywhereSlow changing status: exports, reports
Long pollingGET /events?after=105&wait=30Near instant updates over plain HTTPWhen proxies block SSE or WebSocket
Conditional pollingIf-None-Match: "v7" → 304Unchanged answers cost almost nothingResources that rarely change
Backoffwait 1s, 2s, 4s … up to 30sCuts load while nothing is happeningWaiting on long jobs
Retry-AfterRetry-After: 10The server tells the client when to come backRate limits and busy periods

Try it

poll.shBASH
# short polling: ask every 5 seconds
while true; do
  curl -s https://api.example.com/jobs/77
  sleep 5
done

# long polling: the server waits up to 30 s before answering
cursor=105
while true; do
  resp=$(curl -s --max-time 35 "https://api.example.com/events?after=$cursor&wait=30")
  [ -n "$resp" ] && echo "$resp" && cursor=$(echo "$resp" | jq '.[-1].id')
done

Add jitter to retries. If thousands of clients reconnect at the same second after a deploy, a random delay of a few hundred milliseconds spreads the load.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
bash poll.sh
{"id":77,"status":"queued"}
{"id":77,"status":"running"}
{"id":77,"status":"done","url":"/reports/77.pdf"}
# long polling: one line per real event, nothing in between
[{"id":106,"type":"order.created"}]
[{"id":107,"type":"order.paid"}]

Key terms

TermSimple meaning
IntervalHow long short polling waits between asks
TimeoutHow long the server holds a long poll open
CursorWhere you got up to, so nothing is skipped
Thundering herdEveryone retrying at the same moment
JitterA small random delay that spreads retries
08

Webhooks

A reverse API: instead of you asking, the other service calls your URL the moment something happens.

HTTP POSTJSONHMACServer tells you
In one lineEvent callbacks over HTTP
In simple words

Like giving a shop your phone number so they call when the parcel arrives, instead of you phoning them every hour.

Under the hood

The provider POSTs an event as JSON to an HTTPS endpoint you registered. Verify every delivery: compute an HMAC SHA-256 of the raw body (plus a timestamp, to stop replays) with the shared secret and compare in constant time. Answer 2xx within a few seconds and do the real work from a queue. Providers retry with backoff when you fail, so the same event can arrive twice or out of order; store the event id and skip duplicates. The Standard Webhooks spec describes the headers.

Pick it forReacting to payments, git pushes, form submissions or any event in someone else's system.

Transport
HTTPS POST to you
Payload
JSON event
Direction
Provider calls you
Trust
HMAC signature header

How a conversation looks

Acknowledge fast, process later
Provider
Your API
Queue
POST /webhooks/payments · X-Signature
verify HMAC, check the event id
enqueue job evt_301
200 OK, within seconds
no 2xx? retry with backoff

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Event deliveryPOST /webhooks {"type","id","data"}Push instead of pollingPayments, git pushes, form submissions
Signature checkHMAC-SHA256(secret, raw body)Proves the sender and that nothing was alteredEvery delivery
Timestamp checkreject if older than 5 minutesStops an attacker replaying a captured messageEvery delivery, with the signature
Idempotent handlingskip if event.id already storedRetries mean the same event can arrive twiceEvery consumer
Queue then 200queue.add(…); res.sendStatus(200)Providers time out after a few secondsAny real processing
Thin events{"type","id"} then GET /payments/:idYou always read fresh data, and payloads leak lessWhen order or staleness matters

Try it

webhook.jsJS
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.WEBHOOK_SECRET;

app.post("/webhooks/payments", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.get("X-Signature") ?? "";
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const ok = sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  queue.add("payment", event, { jobId: event.id }); // same id, no duplicate job
  res.sendStatus(200);
});
app.listen(3000);

Sign the raw bytes. Parse the JSON first and re-serialize it, and spacing or key order can change, so the signature never matches.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
body='{"id":"evt_301","type":"payment.captured","amount":149900}'
sig=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | cut -d' ' -f2)
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:3000/webhooks/payments -H "Content-Type: application/json" -H "X-Signature: $sig" -d "$body"
200
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:3000/webhooks/payments -H "Content-Type: application/json" -H "X-Signature: forged" -d "$body"
401

Key terms

TermSimple meaning
EndpointYour URL that receives the events
SignatureProof the message came from the provider
Replay attackResending an old valid message; timestamps stop it
IdempotencyHandling the same event twice changes nothing
BackoffRetries that wait longer each time
09

Server-Sent Events

One HTTP response that never ends: the server keeps writing events, and the browser reconnects by itself.

HTTPtext/event-streamEventSourceServer tells you
In one lineOne way server push
In simple words

Like a radio station. You tune in once and the server keeps broadcasting to you. You can listen, but you cannot talk back on the same channel; for that, send a normal request.

Under the hood

The client sends GET with Accept: text/event-stream; the server answers 200 with Content-Type: text/event-stream and keeps writing UTF-8 frames made of data:, event:, id: and retry: lines, each event ending with a blank line. The browser's EventSource reconnects on drop and sends Last-Event-ID so the server can resume. Text only and one direction. On HTTP/1.1 browsers allow about six connections per site, which HTTP/2 removes. Turn off proxy buffering. LLM token streaming uses SSE.

Pick it forLive feeds, notifications, dashboards and streaming AI responses.

Transport
HTTP, one long response
Payload
UTF-8 text events
Direction
Server to client only
Reconnect
Built in, with Last-Event-ID

How a conversation looks

One request, endless events, automatic resume
Browser
Server
GET /stream · Accept: text/event-stream
200 · the response stays open
id: 41 · event: price · data: {…}
id: 42 · event: price · data: {…}
connection drops, EventSource waits retry ms
GET /stream · Last-Event-ID: 42

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
EventSourcenew EventSource("/stream")Built into browsers, reconnects by itselfBrowsers receiving updates
Named eventsevent: price + addEventListener("price")Routes each type to its own handlerSeveral kinds of event on one stream
Resume by idid: 42 → Last-Event-ID: 42No gaps after a reconnectFeeds where missing an event matters
retryretry: 5000The server sets the reconnect delayShedding load during an incident
fetch streamingfetch(url, { method: "POST" }) + body.getReader()EventSource cannot send a body or custom headersAI chat completions with auth headers
Heartbeat: ping every 15 sKeeps proxies from closing a quiet streamBehind load balancers and CDNs

Try it

sse.jsJS
// server.js, Node with no framework
import http from "node:http";

http.createServer((req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  let id = Number(req.headers["last-event-id"] ?? 0);
  const timer = setInterval(() => {
    id += 1;
    res.write(`id: ${id}\nevent: price\ndata: {"symbol":"NIFTY","ltp":${24800 + id}}\n\n`);
  }, 1000);
  req.on("close", () => clearInterval(timer));
}).listen(3000);

// browser
const es = new EventSource("/stream");
es.addEventListener("price", (e) => console.log(JSON.parse(e.data)));

Send a comment every 15 seconds. A line starting with a colon, such as : ping, keeps proxies from closing an idle stream and is ignored by EventSource.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -N localhost:3000/stream
id: 1
event: price
data: {"symbol":"NIFTY","ltp":24801}

id: 2
event: price
data: {"symbol":"NIFTY","ltp":24802}
# a new event every second until Ctrl+C

Key terms

LineMeans
data: …The payload; several data lines join with newlines
event: nameWhich listener receives it; default is message
id: 42Bookmark the browser sends back on reconnect
retry: 5000Milliseconds to wait before reconnecting
: commentIgnored; useful as a keep alive
10

WebSocket

Upgrade one HTTP connection into a two way pipe where either side can send a message at any moment.

TCPframesfull duplexTalk both ways
In one lineFull duplex messaging
In simple words

Like a phone call instead of letters. Once connected, both people can talk whenever they like, without dialling again for every sentence.

Under the hood

It opens as an HTTP/1.1 GET with Upgrade: websocket and a random Sec-WebSocket-Key; the server replies 101 Switching Protocols with Sec-WebSocket-Accept, the base64 SHA-1 of the key plus a fixed GUID. After that the TCP connection carries frames: text or binary messages (masked from client to server), ping and pong heartbeats, and a close frame with a code. There is no built in reconnect, rooms, acknowledgements or backpressure; Socket.IO adds those. Scale out with sticky sessions and a pub/sub backplane such as Redis.

Pick it forChat, multiplayer, collaborative editing, trading screens: anything where the client also sends often.

Transport
TCP after an HTTP upgrade
Payload
Text or binary frames
Direction
Both ways, any time
URLs
ws:// · wss:// (TLS)

How a conversation looks

One upgrade, then free traffic in both directions
Client
Server
GET /chat · Upgrade: websocket
101 Switching Protocols
{"type":"join","room":"ops"}
{"type":"msg","text":"deploy done"}
ping
pong
close 1000

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Text framesws.send(JSON.stringify(msg))Readable and easy to debugChat, events, commands
Binary framesws.send(arrayBuffer)Compact, no text encoding costAudio, game state, protobuf
Ping and pongsocket.ping() every 30 sFinds dead connections that never closedLong lived connections behind NAT
Subprotocolnew WebSocket(url, ["graphql-transport-ws"])Both sides agree on a message format in the handshakeGraphQL subscriptions, STOMP
Close codesws.close(1000, "bye")Tells the peer why the connection endedClean shutdowns; 1001 during deploys
Socket.IOio.to("ops").emit("msg", data)Adds rooms, acks and reconnects on topApps that need those out of the box

Try it

ws.jsJS
// server.js  (npm i ws)
import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket) => {
  socket.on("message", (raw) => {
    const msg = JSON.parse(raw);
    for (const client of wss.clients) {
      if (client.readyState === 1) client.send(JSON.stringify({ from: "server", echo: msg }));
    }
  });
});

// browser
const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => ws.send(JSON.stringify({ type: "join", room: "ops" }));
ws.onmessage = (e) => console.log(JSON.parse(e.data));

Plan for reconnects yourself. A laptop lid closing gives close code 1006 with no frame. Reconnect with backoff and resend what the server missed.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -si -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" localhost:8080
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
npx wscat -c ws://localhost:8080
Connected (press CTRL+C to quit)
> {"type":"join","room":"ops"}
< {"from":"server","echo":{"type":"join","room":"ops"}}

Key terms

Term or codeSimple meaning
101The server agreed to switch to WebSocket
FrameOne unit of data on the wire
Ping / pongHeartbeat that proves the other side is alive
1000Normal close
1006Dropped without a close frame
BackplanePub/sub that lets many servers share messages
11

MQTT

A tiny publish and subscribe protocol for devices on weak networks: publish to a topic, and every subscriber gets a copy.

TCPbrokertopicsThrough a broker
In one lineLightweight pub/sub
In simple words

Like a noticeboard in a building. A sensor pins a note under home/kitchen/temp, and everyone who asked to watch that heading gets a copy. Nobody needs to know who else is there.

Under the hood

A binary protocol over TCP (1883, 8883 with TLS) or WebSocket, with a fixed header as small as 2 bytes. Clients CONNECT to a broker such as Mosquitto, EMQX or HiveMQ with a keep alive, PUBLISH to slash separated topics and SUBSCRIBE with wildcards: + for one level, # for the rest. QoS 0 is at most once, 1 at least once (PUBACK), 2 exactly once (a four step handshake). Retained messages give newcomers the last value, and a Last Will is published if a client vanishes. MQTT 5 adds reason codes, shared subscriptions and message expiry.

Pick it forIoT sensors, phones on mobile data, vehicles: many small devices on unreliable links.

Transport
TCP, or WebSocket
Payload
Bytes, any format
Direction
Publish and subscribe
Ports
1883 · 8883 (TLS)

How a conversation looks

The sensor and the dashboard never talk directly
Sensor
Broker
Dashboard
SUBSCRIBE home/+/temp
SUBACK
PUBLISH home/kitchen/temp 24.6 · QoS 1
PUBACK
PUBLISH home/kitchen/temp 24.6

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
QoS 0mosquitto_pub -q 0Fastest, no acknowledgementFrequent telemetry where one lost reading is fine
QoS 1mosquitto_pub -q 1Guaranteed delivery, duplicates possibleMost commands and events, with idempotent handlers
QoS 2mosquitto_pub -q 2Exactly once through a four step handshakeMetering or billing where a duplicate costs money
Retainedmosquitto_pub -rNew subscribers get the last value at onceDevice state and config
Last Will--will-topic devices/pi-01/statusThe broker announces a client that vanishedPresence and health
Shared subscription$share/workers/jobs/#Spreads messages across a group of consumersScaling backend processors on MQTT 5

Try it

mqtt.shBASH
# terminal 1: watch every room's temperature
mosquitto_sub -h localhost -t "home/+/temp" -v

# terminal 2: publish a reading and keep it as the last known value
mosquitto_pub -h localhost -t "home/kitchen/temp" -m "24.6" -q 1 -r

# a Last Will: the broker announces it if this client disappears
mosquitto_sub -h localhost -t "home/#" --will-topic "devices/pi-01/status" --will-payload "offline" -v

Design topics like folders. Put the broad part first, site/building/room/metric, so wildcards can select whole branches.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
mosquitto_sub -h localhost -t "home/+/temp" -v
home/kitchen/temp 24.6
home/bedroom/temp 22.1
# a reading published with -r reaches new subscribers at once
home/kitchen/temp 24.9

Key terms

QoS or termSimple meaning
QoS 0At most once: fire and forget
QoS 1At least once: may duplicate
QoS 2Exactly once: slowest
+ and #One level, or every level below
RetainedThe broker keeps the last message per topic
Last WillA message sent for you if you drop off
12

AMQP and RabbitMQ

Reliable message queues: producers send to exchanges, routing rules fill queues, and consumers acknowledge every message.

TCPexchangesacksThrough a broker
In one lineMessage queuing protocol
In simple words

Like a post office sorting room. You hand a letter to the counter (the exchange), sorting rules drop it into the right pigeonhole (the queue), and the worker who takes it signs for it. If the worker drops it, it goes back for someone else.

Under the hood

AMQP 0-9-1, RabbitMQ's main protocol, runs over TCP on 5672 (5671 with TLS) with many channels per connection. Exchanges route by type: direct (exact routing key), topic (patterns with * and #), fanout (every bound queue) and headers. Bindings connect exchanges to queues. Consumers ack, nack or requeue; prefetch caps unacknowledged messages per consumer; durable queues, persistent messages and publisher confirms give at least once delivery; dead letter exchanges catch failures. AMQP 1.0 is a different standard that RabbitMQ 4 also speaks.

Pick it forBackground jobs and work that must not be lost: emails, payments, image processing.

Transport
TCP
Payload
Bytes plus properties
Direction
Producer to queue to worker
Ports
5672 · 5671 (TLS)

How a conversation looks

The producer never needs to know who does the work
Producer
Exchange
Queue
Worker
publish · key order.created
route via binding order.*
deliver (prefetch 10)
ack
on nack: retry, then dead letter queue

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
directbindQueue("emails", "tasks", "email")Exact routing by keyWork queues per task type
topic"order.*" or "order.#"Pattern routing on dotted keysDomain events
fanoutassertExchange("logs", "fanout")Copies every message to every bound queueBroadcasts, cache invalidation
Publisher confirmsawait conn.createConfirmChannel()The broker acknowledges each message it storedMessages you cannot afford to lose
Dead letter exchange{ deadLetterExchange: "dlx" }Catches rejected and expired messagesRetries and poison messages
Prefetchch.prefetch(10)Fair dispatch and bounded memory per workerEvery consumer

Try it

orders.jsJS
import amqp from "amqplib";

const conn = await amqp.connect("amqp://localhost");
const ch = await conn.createChannel();

await ch.assertExchange("orders", "topic", { durable: true });
await ch.assertQueue("email-jobs", { durable: true });
await ch.bindQueue("email-jobs", "orders", "order.*");

// producer
ch.publish("orders", "order.created", Buffer.from(JSON.stringify({ id: "o_981" })), { persistent: true });

// consumer
await ch.prefetch(10);
await ch.consume("email-jobs", (msg) => {
  console.log("send email for", JSON.parse(msg.content.toString()).id);
  ch.ack(msg);
});

Ack after the work, not before. Acknowledge once the email is sent. If the worker crashes first, RabbitMQ redelivers the message to another worker.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
node orders.js
send email for o_981
sudo rabbitmqctl list_queues name messages consumers
Timeout: 60.0 seconds ...
Listing queues for vhost / ...
name	messages	consumers
email-jobs	0	1

Key terms

Exchange typeRoutes to
directQueues whose binding key equals the routing key
topicPattern matches: order.* or order.#
fanoutEvery bound queue, key ignored
headersMatches on message header values
13

Kafka protocol

A distributed commit log: producers append events to partitioned topics, and each consumer group reads at its own pace, as often as it likes.

TCPpartitionsoffsetsThrough a broker
In one lineEvent streaming log
In simple words

Like a ledger that only gains new lines at the bottom. Any team can read it, keep its own bookmark and reread old pages later. Reading never erases anything.

Under the hood

Kafka uses its own binary request and response protocol over TCP, usually port 9092. Topics split into partitions, each an ordered append only log replicated to other brokers (leader plus in sync replicas). The producer picks a partition by hashing the key, so one key keeps its order; acks=all with the idempotent producer avoids duplicates. Consumers in a group share the partitions and commit offsets; joining or leaving triggers a rebalance. Data stays for a retention period or is compacted per key. Kafka 4 runs on KRaft without ZooKeeper.

Pick it forEvent driven systems, audit trails, analytics pipelines, and many consumers of the same events.

Transport
TCP, Kafka binary protocol
Payload
Key, value, headers
Direction
Append, then pull
Port
9092

How a conversation looks

Two groups read the same log from different bookmarks
Producer
Broker
Billing group
Analytics group
produce key user42 · order.created
ack, acks=all
fetch from offset 1041
records 1041 to 1050
fetch from offset 980
records 980 to 1050

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
acks=0acks=0Fastest; the producer does not waitMetrics where some loss is acceptable
acks=all + idempotenceacks=all, enable.idempotence=trueNo loss and no duplicates per partitionOrders, payments; the default since Kafka 3.0
Keyed partitioningkey = user42Same key, same partition, kept in orderPer entity event streams
Consumer groupgroup.id = billingPartitions are shared out so the group scalesEach service reading a topic
Log compactioncleanup.policy = compactKeeps the latest event per key foreverChangelogs and state snapshots
Transactionstransactional.id = billing-1Read, process and write atomically for exactly onceStream processing pipelines

Try it

kafka.shBASH
# produce with a key: same key, same partition, same order
echo 'user42:{"type":"order.created","id":"o_981"}' | kcat -b localhost:9092 -t orders -K: -P

# consume as part of the billing group
kcat -b localhost:9092 -G billing orders

# read the topic like a file, with partition and offset
kcat -b localhost:9092 -t orders -C -o beginning -f '%p:%o %k %s\n' -e

# how far behind is each group?
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group billing

Order is per partition, not per topic. Give related events the same key, such as the user id, so they land in one partition and stay in sequence.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
kcat -b localhost:9092 -t orders -C -o beginning -f '%p:%o %k %s\n' -e
0:1041 user42 {"type":"order.created","id":"o_981"}
% Reached end of topic orders [0] at offset 1042: exiting
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group billing
GROUP    TOPIC   PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
billing  orders  0          1042            1042            0

Key terms

TermSimple meaning
TopicA named stream of events
PartitionOne ordered slice of a topic
OffsetAn event's position, the bookmark
Consumer groupWorkers that share a topic's partitions
LagHow many events a group has not read yet
CompactionKeep only the latest event per key
14

WebRTC

Real time audio, video and data directly between browsers, with servers only helping the two peers find each other.

UDPpeer to peerDTLS-SRTPTalk both ways
In one linePeer to peer media and data
In simple words

Like two people swapping addresses through a mutual friend, then talking directly. The friend is the signaling server; a STUN server tells each person their public address; if the walls are too thick, a TURN server relays the call.

Under the hood

Each side creates an RTCPeerConnection and exchanges an SDP offer and answer over your own signaling channel, often a WebSocket. ICE gathers candidate addresses (host, server reflexive from STUN, relay from TURN) and tests pairs until one connects through the NATs. Media is always encrypted with DTLS-SRTP, and data channels run SCTP over DTLS, reliable or unordered. Calls with many people route through an SFU such as LiveKit or mediasoup instead of a full mesh.

Pick it forVideo calls, screen sharing, low latency game data and browser to browser file transfer.

Transport
UDP, falls back to TCP or TURN
Payload
Audio, video, data channels
Direction
Peer to peer, both ways
Security
Encryption is mandatory

How a conversation looks

Servers introduce the peers; the call itself goes direct
Peer A
Signaling
STUN
Peer B
what is my public address?
203.0.113.7:54012
SDP offer + ICE candidates
offer
SDP answer
answer
direct media and data · DTLS-SRTP

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Media trackspc.addTrack(track, stream)Low latency audio and video with built in jitter handlingCalls and live streaming
Reliable data channelpc.createDataChannel("chat")Ordered with retransmits, like TCPChat and file transfer
Unordered data channelcreateDataChannel("game", { ordered: false, maxRetransmits: 0 })The latest update wins and nothing stallsGame state, live cursors
STUN{ urls: "stun:stun.l.google.com:19302" }Each peer learns its public addressAlways
TURN{ urls: "turn:turn.example.com:3478", username, credential }Relays traffic when no direct path existsProduction, especially corporate networks
SFULiveKit, mediasoup, JanusEach peer uploads once and the server forwardsGroup calls beyond about four people

Try it

peer-a.jsJS
const pc = new RTCPeerConnection({
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
const chat = pc.createDataChannel("chat");
chat.onopen = () => chat.send("hello from A");

pc.onicecandidate = ({ candidate }) => candidate && signal.send({ candidate });

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signal.send({ sdp: pc.localDescription }); // signal is your WebSocket

signal.onmessage = async ({ sdp, candidate }) => {
  if (sdp) await pc.setRemoteDescription(sdp);
  if (candidate) await pc.addIceCandidate(candidate);
};

Budget for TURN in production. Roughly one call in ten cannot connect directly behind strict corporate or mobile NATs, and only a TURN relay rescues it.

browser consoleChrome
# a recorded session, replayed when you press Run
# browser console on peer A
candidate:842163049 1 udp 1677729535 203.0.113.7 54012 typ srflx raddr 192.168.1.20 rport 54012
connectionState: connecting
connectionState: connected
chat: hello from B

Key terms

TermSimple meaning
SDPA description of what each side can send and receive
SignalingHow peers swap SDP; WebRTC leaves it to you
ICEThe process of finding a path between peers
STUNTells a peer its public address
TURNRelays traffic when no direct path exists
SFUA server that forwards streams in group calls

Which one should I pick?

Read across the row for the job you have. When two look equal, pick the one your team and your clients already speak.

ProtocolTransportPayloadDirectionIn a browserPick it when
RESTHTTPJSONAsk and answerYesPublic and CRUD APIs
SOAPHTTPXMLAsk and answerAwkwardEnterprise systems with a WSDL
GraphQLHTTPJSONAsk and answerYesScreens that need custom data shapes
gRPCHTTP/2ProtobufUnary or streamsVia gRPC-WebFast typed service to service calls
tRPCHTTPJSONAsk and answerYesOne TypeScript team, front and back
Long pollingHTTPAnyServer, slowlyYesFallback when push is blocked
WebhooksHTTP POSTJSONProvider to youNoEvents from another system
SSEHTTPTextServer to clientYesFeeds, notifications, AI streaming
WebSocketTCPText or binaryBoth waysYesChat, games, collaboration
MQTTTCPBytesPub/subOver WebSocketIoT and weak networks
AMQPTCPBytesQueue to workerNoJobs that must not be lost
KafkaTCPRecordsAppend, then pullNoEvent streams many teams replay
WebRTCUDPMedia and dataPeer to peerYesCalls and low latency data