Reference · gazar.dev ← All cheat sheets

APIs · networking

The API design & networking cheat sheet.

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.

01

The anatomy of an API call

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

The trap

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.

02

REST vs gRPC vs GraphQL

Three styles, three sweet spots. The question is never "which is best" but "which fits this caller and this boundary."

StyleShapeBest forCost
RESTresources over HTTP/JSON, verbs + status codespublic APIs, broad compatibility, cacheable readsover/under-fetching; many round trips for graphs
gRPCbinary protobuf over HTTP/2, typed RPCinternal service-to-service, low latency, streamingnot browser-native; binary is harder to debug
GraphQLone endpoint, client picks the fieldsvaried clients, deep graphs, avoiding over-fetchcaching is hard; N+1 and query-cost risks

The one-line pick

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.

03

Contract design that ages well

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.

Idempotencysafe to retry
Do
GET/PUT/DELETE are idempotent by definition; make POST safe with an idempotency key the server dedupes on.
Watch
a non-idempotent POST behind a retrying client double-charges. The network will retry, so design for it.
Paginationbounded responses
Do
prefer cursor (keyset) pagination over offset: stable under inserts and fast deep in the list.
Watch
offset/limit drifts when rows are inserted and gets slow at high offsets (the DB still scans them).
Versioningchange without breaking
Do
version the contract (/v1, header, or media type) and add fields additively. Never repurpose a field.
Watch
a "small" type or meaning change is a breaking change for someone. Deprecate on a timeline, don't yank.
Rate limitingprotect the service
Do
token-bucket per client; return 429 with Retry-After and the limit headers so clients can back off.
Watch
no limits means one client can starve the rest; silent throttling without headers makes clients hammer harder.
04

Status codes & error shapes

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.

ClassMeansCommonRetryable?
2xxsuccess200, 201, 204n/a
3xxredirect / not modified301, 304n/a
4xxcaller's fault400, 401, 403, 404, 409, 422, 429no (except 429, later)
5xxserver's fault500, 502, 503, 504yes, 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.

The line that matters

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.

05

HTTP/1.1 vs 2 vs 3

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.

VersionTransportConcurrencyHead-of-line blocking
HTTP/1.1TCPone request per connection (pipelining unused)at the application layer; clients open 6 connections to fake parallelism
HTTP/2TCPmultiplexed streams on one connectionsolved in HTTP, but one lost TCP packet still stalls all streams
HTTP/3QUIC (UDP)multiplexed streams, independentgone; a lost packet stalls only its own stream; faster handshake

Why HTTP/3 exists in one line

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.

06

DNS, TCP, TLS: the cost before byte one

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.
07

Keep-alive, pooling & multiplexing

The single biggest latency win in practice: stop re-paying the handshake. Three related mechanisms do it at different layers.

Keep-alivereuse one connection
What
hold the TCP/TLS connection open after a response so the next request skips the handshakes.
Watch
idle connections cost memory; tune the idle timeout so you reuse hot ones without hoarding dead ones.
Connection poolingclient-side
What
keep a bounded pool of warm connections (to a DB or upstream) and hand them out, instead of dialing per call.
Watch
size the pool to the dependency; an unbounded pool just moves the overload downstream (ties to the bulkhead in S9).
MultiplexingHTTP/2 & 3
What
many concurrent requests share one connection as independent streams, no need for six parallel sockets.
Watch
on HTTP/2 a lost TCP packet stalls all streams; HTTP/3 over QUIC fixes that.
08

Load balancing & the edge

How a request actually reaches one of your instances, and where to cache and bound it before it gets there.

LayerSeesCan route byUse for
L4 LBTCP/UDP, IP + portconnection, IP hashraw throughput, non-HTTP, lowest overhead
L7 LBHTTP: path, headers, cookiesURL, host, header, sticky sessionrouting, TLS termination, retries, canary
CDNHTTP at the edge, near the usercache key, geostatic + cacheable content, TLS close to user

Timeouts & retries at the edge

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.

09

Interview questions, with crisp answers

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.

10

Traps & the repeatable move

The ways APIs and their networks go wrong, and the habit that catches them.

The traps

  • Non-idempotent POST with retries, the network retries and double-charges.
  • Offset pagination at scale, drifts under inserts, slows at depth.
  • Repurposing a field, a silent breaking change for every caller.
  • Branching on the error message string, use a stable code instead.
  • No keep-alive / pooling, paying DNS + TCP + TLS on every call.
  • Timeouts only deep in the service, a slow upstream holds the edge connection.
  • GraphQL with no query-cost limit, one deep query melts the backend (N+1).

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.

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.