Day 17 of 100·System Design Day·569 words

HTTP internals, gRPC, and TCP vs UDP

What actually happens on the wire — and why both your systems chose plain HTTP/JSON.

Concept → your projects

Know the layers cold enough to debug them:

  • TCP: connection (3-way handshake), ordered reliable byte stream, congestion control (slow start → AIMD), head-of-line blocking within a connection. UDP: datagrams, no handshake, no ordering — the base for DNS, QUIC, video.
  • HTTP/1.1: keep-alive avoids per-request handshakes; one request per connection at a time → browsers open ~6 parallel connections → HOL blocking at the app layer. HTTP/2: one connection, binary frames, multiplexed streams (fixes app-layer HOL, inherits TCP HOL), header compression (HPACK), server push (dead). HTTP/3: QUIC over UDP — moves reliability into the protocol, kills transport HOL, 0/1-RTT handshake. (Browsers → your sites already speak H2/H3 at the CDN edge; your origin servers are H1.1 — fine.)
  • REST (your choice, both projects): resource-oriented, cacheable, debuggable with curl, ubiquitous tooling. gRPC: HTTP/2 + protobuf IDL, typed stubs, bidirectional streaming, built-in deadline/cancel propagation — the standard for internal service-to-service at scale. Costs: binary payloads (hard to debug without tooling), codegen per language, browser-unfriendly (needs grpc-web proxy).

Why zoth is HTTP/JSON internally, and why that's correct: three services, ~1 rps, one team. gRPC's wins (typed contracts, streaming, deadline propagation) matter at 50 services and 50 teams. The emitter doesn't even call the API over HTTP — they coordinate through Postgres + RabbitMQ (the strongest answer to "why not gRPC": no synchronous inter-service calls exist to optimize). One legit future gRPC site: zlink connectors to platform APIs — but those are external REST APIs; you don't get to choose.

Your HTTP details worth knowing cold

Zoth:

  • 15 s read/write timeouts on http.Server (slowloris defense; goroutine leak prevention).
  • otelhttp middleware wraps every request in a span; W3C traceparent propagates via the composite TraceContext+Baggage propagator.
  • Health checks dial RabbitMQ with a raw TCP dial — not an AMQP handshake — deliberately cheap.
  • tokenTransport in the GitHub zlink wraps http.RoundTripper to inject Authorization: Bearer — the idiomatic Go pattern for per-client auth (know it: it's how you'd add retries/metrics to any SDK).

Traverse:

  • express.json({limit:'10mb', verify: rawBody}) — the raw bytes are kept so Razorpay's webhook HMAC can be verified against the exact body (re-stringified JSON would break the signature — a classic production bug you avoided by design).
  • trust proxy 1 — exactly one proxy hop assumed, so req.ip unwraps X-Forwarded-For once; setting it to true blindly would let clients spoof IPs past your rate limiter (validate: xForwardedForHeader: false in the limiter config).
  • APNs push over raw HTTP/2 with jose (src/lib/apns.ts) — ES256 provider JWT, sessions pooled per origin, because sandbox + production coexist per-token. That's you already using HTTP/2 deliberately.
  • Webhooks return 200-always (Razorpay) to stop provider retries — idempotency absorbs re-delivery instead.

TCP-vs-UDP interview questions, answered like an owner

"Why is DNS UDP?" — single small question/response fits one datagram; TCP handshake would triple latency; fallback to TCP for >512 B or zone transfers.

"Your WebSocket would use which?" — TCP (HTTP/1.1 Upgrade) — you'd never hand-roll reliability on UDP; QUIC is the modern exception.

"When is UDP right in backend systems?" — telemetry fire-and-forget, live video, game state — anywhere late > lost. Your suspicious-activity events are effectively fire-and-forget at-least-once over TCP — correct, because they're rare and cheap; a metrics firehose would justify UDP.

Bulletproof your resume

Say these answers aloud before moving on

These are the follow-ups an interviewer will actually ask about your projects.

01

Why not gRPC between zoth services?

There are no synchronous inter-service calls to upgrade: the API writes to Postgres, the emitter reads Postgres and consumes RabbitMQ, alerting publishes to RabbitMQ. Coordination is data and events, not RPCs. gRPC would add codegen, binary debugging, and HTTP/2 enforcement for zero calls. If approval lookups ever moved to a separate identity service called per-request, that's the moment gRPC + deadline propagation earns its cost — and I'd say protobuf contracts beat JSON there because callers get compile-time breakage instead of runtime 500s.

02

Why did traverse keep the raw request body for the payment webhook?

HMAC signature verification must run over the exact bytes the provider signed. express.json re-serializes parsed objects — key order, whitespace, unicode escaping can all differ, breaking the signature. The verify callback in the json parser stores req.rawBody before parsing, so the webhook handler compares HMAC-SHA256 against the original bytes. The same principle applies to zoth's WorkOS webhook: verify against the raw body, with a 5-minute timestamp window for replay defense.

03

Explain head-of-line blocking and where HTTP/3 fixes it.

Two levels: application (HTTP/1.1 — one outstanding request per connection, so a slow response blocks the queue; fixed by H2 multiplexing) and transport (TCP delivers bytes in order, so one lost packet stalls all multiplexed streams; fixed by QUIC mapping streams to independent UDP packet handling — loss on stream A doesn't block stream B). For my systems this is irrelevant at current traffic, but APNs already uses H2, and any high-fanout frontend (the traverse dashboard) benefits at the browser edge today.