APIs · networking
Two halves of one request: the contract you design and the network it travels. This sheet is both on one page: REST vs gRPC vs GraphQL, the contract details that bite (idempotency, pagination, versioning, errors), and the path underneath, DNS, TCP, TLS, HTTP versions, load balancers and the CDN. Companion to S5 · API Design & Networking Fundamentals.
An API is a promise, and the network is how it travels. The contract is the part your callers depend on forever, so design it to outlive your implementation. The network underneath, handshakes, HTTP version, load balancer, edge, decides the latency and the failure modes you inherit whether you think about it or not. Senior work is owning both halves on purpose.
Before any choice, see the whole path. A request resolves a name, opens a connection, climbs through load balancers and a CDN, and finally hits your contract. Each hop adds latency and a failure mode. Knowing the path is how you reason about both.
flowchart TB C["client"] --> DNS["DNS lookup"] DNS --> TCP["TCP handshake"] TCP --> TLS["TLS handshake"] TLS --> REQ["HTTP request"] REQ --> CDN["CDN edge: cache hit return, miss forward"] CDN --> LB["L7 load balancer"] LB --> SVC["your service"] SVC --> CONTRACT["the CONTRACT: REST, gRPC, GraphQL"] classDef accent fill:#DCEBFE,stroke:#4A90D9,color:#09244B class CONTRACT accent
Designing the contract and treating the network as someone else's problem. The two interact: a chatty REST design that needs ten round trips is slow precisely because each trip pays the network cost from section 6. Design the contract with the path in mind.
Three styles, three sweet spots. The question is never "which is best" but "which fits this caller and this boundary."
| Style | Shape | Best for | Cost |
|---|---|---|---|
| REST | resources over HTTP/JSON, verbs + status codes | public APIs, broad compatibility, cacheable reads | over/under-fetching; many round trips for graphs |
| gRPC | binary protobuf over HTTP/2, typed RPC | internal service-to-service, low latency, streaming | not browser-native; binary is harder to debug |
| GraphQL | one endpoint, client picks the fields | varied clients, deep graphs, avoiding over-fetch | caching is hard; N+1 and query-cost risks |
REST for a public, cacheable, broadly compatible API. gRPC between your own services where you control both ends and want speed and streaming. GraphQL when many different clients each need a different slice and round trips hurt. Plenty of systems use REST or gRPC at the edge and gRPC internally.
The details callers depend on forever. Get these right and the API survives years of change; get them wrong and every change is a breaking one. Each card: do the rule, watch the trap.
/v1, header, or media type) and add fields additively. Never repurpose a field.429 with Retry-After and the limit headers so clients can back off.Use the codes the way the whole web expects, and give errors a consistent, machine-readable shape. This is half of how callers tell a retryable failure from a permanent one.
| Class | Means | Common | Retryable? |
|---|---|---|---|
| 2xx | success | 200, 201, 204 | n/a |
| 3xx | redirect / not modified | 301, 304 | n/a |
| 4xx | caller's fault | 400, 401, 403, 404, 409, 422, 429 | no (except 429, later) |
| 5xx | server's fault | 500, 502, 503, 504 | yes, with backoff |
A consistent error shape (RFC 9457 problem+json)
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient funds",
"status": 402,
"detail": "Wallet balance 4.20 is below the 9.99 charge",
"instance": "/charges/a1b2",
"code": "INSUFFICIENT_FUNDS" # stable, machine-readable
}
=> clients branch on code, humans read detail, never parse the message string.
Pick 409 vs 422 deliberately: 409 for a conflict with current state (duplicate, version mismatch), 422 for a well-formed request that fails a business rule. And put a stable code in the body, clients should branch on that, never on the human-readable message.
The version decides how many requests share a connection and where they block. Each step removed a head-of-line blocking problem from the one before.
| Version | Transport | Concurrency | Head-of-line blocking |
|---|---|---|---|
| HTTP/1.1 | TCP | one request per connection (pipelining unused) | at the application layer; clients open 6 connections to fake parallelism |
| HTTP/2 | TCP | multiplexed streams on one connection | solved in HTTP, but one lost TCP packet still stalls all streams |
| HTTP/3 | QUIC (UDP) | multiplexed streams, independent | gone; a lost packet stalls only its own stream; faster handshake |
HTTP/2 multiplexes many streams over one TCP connection, but TCP delivers in order, so a single lost packet stalls every stream (transport-level head-of-line blocking). HTTP/3 runs over QUIC on UDP, where streams are independent, so a lost packet only stalls its own stream, and the handshake folds in TLS for fewer round trips.
Before your server sees a single request byte, the client has paid for a name lookup and two handshakes. This is why connection reuse (next section) matters so much: these costs are per new connection.
sequenceDiagram participant C as Client participant R as DNS resolver participant S as Server C->>R: DNS lookup name R-->>C: IP, cold lookup is 1 round trip C->>S: TCP SYN S-->>C: SYN-ACK C->>S: ACK, 1 round trip to open C->>S: TLS 1.3 handshake, 1 round trip S-->>C: TLS established C->>S: only now HTTP request S-->>C: response Note over C,S: new connection cost is 2 to 3 round trips BEFORE your app runs. Reuse and pay once, not per request.
A cold request pays DNS + TCP + TLS before your code runs, often more latency than the request itself across a continent. TLS 1.3 cut the handshake to one round trip (and 0-RTT resumption for repeat visitors). The practical lever is to not pay it every time: keep connections alive and pool them.
The single biggest latency win in practice: stop re-paying the handshake. Three related mechanisms do it at different layers.
How a request actually reaches one of your instances, and where to cache and bound it before it gets there.
| Layer | Sees | Can route by | Use for |
|---|---|---|---|
| L4 LB | TCP/UDP, IP + port | connection, IP hash | raw throughput, non-HTTP, lowest overhead |
| L7 LB | HTTP: path, headers, cookies | URL, host, header, sticky session | routing, TLS termination, retries, canary |
| CDN | HTTP at the edge, near the user | cache key, geo | static + cacheable content, TLS close to user |
Set timeouts at the load balancer and client, not just deep in the service, so a slow upstream can't hold an edge connection forever. Retry only idempotent requests, with backoff and a budget, and prefer the LB to retry a failed instance over the client retrying the whole path. A CDN in front turns most read traffic into edge cache hits that never touch your origin at all.
The API and networking questions that come up, and the sentence or two that shows judgement rather than recall.
Q · REST, gRPC or GraphQL?
REST for public, cacheable APIs. gRPC between your own services for speed and streaming. GraphQL when many clients each need a different slice and round trips hurt. Match the style to the caller.
Q · Offset vs cursor pagination?
Cursor (keyset) is stable under inserts and stays fast deep in the list. Offset drifts when rows change and slows at high offsets because the DB still scans them. Default to cursor.
Q · How do you version an API?
Add fields additively and never repurpose one. When you must break, version it (path, header, or media type) and deprecate the old on a timeline rather than yanking it.
Q · Why HTTP/3 over HTTP/2?
HTTP/2 multiplexes over TCP, so one lost packet stalls every stream. HTTP/3 runs on QUIC over UDP where streams are independent, removing transport head-of-line blocking and cutting handshake round trips.
Q · What happens before my server sees a request?
DNS resolves the name, TCP does a handshake, TLS does another. Two to three round trips of latency before byte one. Keep-alive and pooling let you pay it once, not per request.
Q · L4 vs L7 load balancer?
L4 routes by IP and port, cheap and protocol-agnostic. L7 reads HTTP, so it can route by path or header, terminate TLS, retry, and do canary. Use L7 when you need HTTP-aware routing.
Q · How do you make a POST safe to retry?
An idempotency key per logical action: the server dedupes on it and replays the stored response. A timeout-then-retry then applies once instead of double-charging.
Q · 409 vs 422?
409 for a conflict with current state (duplicate, version mismatch). 422 for a well-formed request that fails a business rule. Both beat a vague 400 because the caller can act on them.
The ways APIs and their networks go wrong, and the habit that catches them.
The traps
code instead.The repeatable move
1. who is the caller? pick REST / gRPC / GraphQL
2. design the contract to age: idempotent, versioned, paginated
3. mind the network: reuse connections, right HTTP version
4. bound it at the edge: timeouts, retries, cache
The tell in a design review: a write with no idempotency story, offset pagination on a big list, or a timeout that only exists inside the service. Spot it and push back.
Design the contract to outlive the code, and don't pay the network twice. The contract is the promise; idempotency, versioning and consistent errors keep it. The network is the path; connection reuse, the right HTTP version, and edge bounding keep it fast. This sheet is the S5 session on one page.
Companion to S5 · API Design & Networking Fundamentals. Further reading: RFC 9457 · Problem Details for HTTP APIs · gRPC core concepts · GraphQL best practices · RFC 9114 · HTTP/3 · Cloudflare · What is HTTP/3.