Skip to content
Computer Science Fundamentals
Source Code
← Home

Protocols

100 questions

Right
0
Wrong
0
Not answered
100
Difficulty
Number of questions

100 questions match this filter

Theory summary

When you ask a friend to pass you the salt, you both follow hidden rules: you say please, they look at you, they hand it over, you say thanks. If one of you broke the rules, the salt would never arrive. Computers need rules like that too. A protocol is a set of rules that says who speaks first, how a message looks, and what the answer means. Because every program follows the same rules, a browser written in one country can talk to a server written by someone else in another.

This area is about the protocols you use every time you open a web page, watch a video, chat or call an app's server. We start with HTTP, the language of the web: its methods, its numbered answers called status codes, and its notes called headers. Then we see how the web saves time with caches, how cookies remember you, and how browsers protect you with CORS. After that comes TLS, the lock that keeps secrets safe, and the three versions of HTTP (1.1, 2 and 3) that carry the same messages in faster and faster ways.

On top of HTTP people build APIs, which are menus of things a program can ask another program to do. You will meet the styles REST, GraphQL, JSON-RPC and gRPC, and also WebSocket and server-sent events, which let a server talk to you without being asked first. At the end you will know what each one is good at and what each one costs, which is exactly what the quiz asks.

The big picture: talking over a network

Think of a restaurant. You are the client: you ask for things. The waiter takes your order to the kitchen, which is the server: it prepares the thing and sends it back. On the web it is the same. Your browser sends a request ('give me this page') and the server sends a response ('here it is'). The conversation always starts with the client.

Messages do not fly by magic. They travel through layers, like a letter that goes in an envelope, which goes in a bag, which goes on a truck. Each layer has its own job.

  your program     'GET /photo'            <- HTTP (this area)
       |
  encryption       scrambles the bytes     <- TLS
       |
  transport        delivers the bytes      <- TCP or QUIC (UDP)
       |
  the network      carries packets         <- IP, wires, Wi-Fi
The layers a web request crosses, from your program down to the wire.
WordMeaning
ProtocolA set of rules for a conversation between programs.
Reliable, but it makes you wait for the first missing piece.
Fast and simple. QUIC is built on top of it.
The unit we use to count how slow a handshake is.
LatencyThe delay of the network. High latency means every round trip is slow.
↑ Back to the contents

HTTP semantics: methods, status codes and headers

HTTP (HyperText Transfer Protocol) is the language of the web. Every HTTP message has three parts: a first line that says what is being asked (or how it went), then headers, which are small notes, then an optional body, which is the real content, like a photo or some text.

GET /photo.png HTTP/1.1
Host: example.test
Accept: image/png
A request (the invisible end-of-line characters are left out). The first line is the request line: method, target, version.
  • Every line ends with two invisible characters, CR and LF (written \r\n). An empty line means 'the headers are over'.
  • The first line of a response is the status line: the version, a three-digit status code, and a short text.
  • In HTTP/1.1 the Host header is required. One computer can host many sites, and Host says which site you want. Without it (or with two), the server must answer 400 Bad Request.

Methods: the verbs

The method says what you want to do. The official rules (RFC 9110) describe three properties. A method is safe if it only reads and asks for no change. It is idempotent if doing it many times has the same effect as doing it once. It is cacheable if the answer may be stored for reuse.

Every safe method is idempotent, but not the other way round. PUT and DELETE are idempotent but not safe.
MethodWhat it doesSafeIdempotent
GETRead a resourceyesyes
HEADSame as GET, but only the headers come back, with no bodyyesyes
OPTIONSAsk what is allowedyesyes
PUTReplace a resource with what you sendnoyes
DELETERemove a resourcenoyes
POSTSend something to be processed, often to createnono
PATCHChange part of a resourcenono

Idempotent is about the effect, not the answer. The first DELETE may answer 204 and the second 404, and the method is still idempotent, because the final state is the same: the thing is gone.

Status codes: how it went

The first digit tells the family: 1xx information, 2xx success, 3xx go somewhere else, 4xx the client made a mistake, 5xx the server made a mistake.

CodeMeaningWhen
200 / 201 / 204OK / Created / No ContentCreated comes with a Location header with the new URL
206Partial ContentAnswer to a Range request (resume a download)
301, 302, 303, 307, 308Redirects301 and 308 are permanent; 302 and 307 temporary. 307 and 308 keep the method and body; 303 always means 'now GET another page'
304Not ModifiedYour stored copy is still good
400Bad RequestThe message is broken (bad line, missing Host)
401UnauthorizedReally means 'not authenticated': tell me who you are. Comes with WWW-Authenticate
403ForbiddenI know who you are, and the answer is no
404 / 405Not Found / Method Not AllowedNo such path / the path exists but not for that method
406 / 415Not Acceptable / Unsupported Media TypeServer cannot give the Accept format / server does not accept the Content-Type you sent
409 / 412Conflict / Precondition FailedClashes with the current state / an If-Match condition was false
413 / 414 / 431Too largeBody / URL / headers are too big
422Unprocessable ContentRight format, but the content breaks a rule
500 / 501 / 505Server errorBug / not implemented / HTTP version not supported

Where does the body end?

In HTTP/1.1 the connection stays open (this is called keep-alive and it is the default: opening a connection costs a round trip, so we reuse it). The next message comes right after the first one, so the reader must know exactly where each body stops. There are two ways.

QuestionContent-LengthTransfer-Encoding: chunked
Use whenThe size is known before sendingThe size is not known yet (a stream)
How it endsRead exactly that many bytesChunks, each with its size in ; a chunk of size 0 ends it
On the wirehello worldb\r\nhello world\r\n0\r\n\r\n

If a message has both headers, Transfer-Encoding wins and the server should preferably refuse it with 400. Why? If a proxy believes one header and the server behind it believes the other, they disagree about where the request ends, and the leftover bytes are read as a new request. That attack is called request smuggling. Closing the connection at the end of the body also works, but then it cannot be reused.

Useful headers

  • Content-Type describes the body you are sending; Accept says which formats you are willing to receive.
  • ETag is a label (a fingerprint) of the current version of a resource. Sending it back in If-Match on a PUT means 'only write if nobody changed it since I read it'. If someone did, the server answers 412. This prevents the lost update, where two people edit and the second erases the first.
  • Range: bytes=1000- asks for the rest of a file. If-Range: "a" adds: 'only if the version is still "a"'. If it is, you get 206 with the part; if the file changed, you get 200 with the whole new file.
↑ Back to the contents

HTTP caching, cookies and CORS

Caching: do not carry the same water twice

A cache keeps a copy of an answer so the next person who asks gets it fast, without bothering the server. Your browser has a private cache (only for you). A or has a shared cache (for many people). The server controls it with Cache-Control.

Cache-ControlWhat it tells caches
max-age=600The copy is fresh for 600 seconds. During that time it is reused with no network. After that it is stale, and the cache tries to check it, not throw it away
no-cacheYou may store it, but you must ask the server before each reuse
no-storeDo not store it at all (use for secrets)
privateOnly the user's own browser may store it, not shared caches
public, s-maxageShared caches may store it. Only for content that is the same for everyone

When a copy is stale, the cache revalidates it with a conditional GET. It sends the label it saved: If-None-Match: "abc" (from ETag) or If-Modified-Since (from Last-Modified). If nothing changed, the server answers 304 Not Modified with only headers, and the copy is fresh again. If it changed, a 200 brings the new body.

cache copy is stale
   cache  --- GET /app.css  If-None-Match: "abc" --->  server
   cache  <-- 304 Not Modified (no body) -------------  server   (reuse the copy)
   cache  <-- 200 OK + new body + new ETag ----------   server   (it changed)
Revalidation costs one round trip but almost no bytes.

A cache normally finds a copy by method + URL. If the server sends different versions for the same URL (for example gzip or Brotli compression depending on Accept-Encoding), it must add Vary: Accept-Encoding. Then that header becomes part of the key, and a client that only understands gzip never receives Brotli.

Cookies: a sticker the browser remembers

HTTP forgets you between requests. A cookie is a small note the server hands you with Set-Cookie, and your browser sends it back on later requests. That is how a site knows you are logged in.

  • Session cookie: has no Expires or Max-Age. It disappears when the browser session ends.
  • Persistent cookie: has Max-Age (seconds from now) or Expires (a date). It survives closing the browser. If both exist, Max-Age wins.
  • Secure: only sent over HTTPS (limits the channel).
  • HttpOnly: JavaScript cannot read it, only the browser attaches it to requests (limits who reads).
  • SameSite: limits when it is sent on requests that start from another site.
Lax blocks most CSRF attacks and still lets outside links keep you logged in.
SameSiteSent on requests that come from another site?
StrictNever
LaxOnly when the user navigates to your site with a safe method, like clicking a link. Not for images, fetch or a form POST from the other site
NoneAlways (and it requires Secure)

Careful: 'same site' is not 'same origin'. shop.example.com and other.example.net are different sites, so a Lax cookie is not sent when the other page makes a request.

CORS: the browser's neighbourhood rule

The origin of a page is three things together: scheme + host + port. https://app.example.com and https://app.example.com:8443 are different origins. The path does not count. The same-origin policy says a script from one origin may not read answers from another origin. CORS (Cross-Origin Resource Sharing) is how a server opens an exception, using headers such as Access-Control-Allow-Origin.

  • A simple request (GET, HEAD or POST with only safe headers and a form-like Content-Type) is sent straight away, because a page could already do that with a form before CORS existed.
  • Anything else (a JSON Content-Type, a custom header, PUT or DELETE) first triggers a preflight: the browser sends an OPTIONS request to ask 'do you accept this?'.
  • Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true is forbidden. With cookies, the server must name one specific origin (checked against a list) and add Vary: Origin.
↑ Back to the contents

TLS: the lock on the connection

Normal HTTP travels as plain text. Anyone on the path (a Wi-Fi owner, a router) could read it or change it. TLS (Transport Layer Security) wraps the conversation in a lock. HTTP inside TLS is HTTPS. Before any web data moves, the two sides do a short dance called the handshake.

A finished handshake gives three things: they agree on a version and secret-scrambling methods, the client authenticates the server (checks it is really who it says), and both sides end up with the same secret key that nobody else knows. Making the client prove itself with a certificate also exists, but on the web it is optional and rare.

Two kinds of secret-keeping

KindAsymmetricSymmetric
IdeaA public key and a private key that belong togetherOne shared key for both sides
SpeedSlow and expensiveVery fast
Used in TLS forSigning (to prove identity) and (to agree on a secret) during the handshakeProtecting all the data afterwards, with an AEAD mode that gives secrecy and tamper detection together

How the client trusts the server

The server shows a certificate: an ID card with its name and public key, signed by a certificate authority (CA). Usually the chain has three links: a root CA signs an intermediate, which signs the site's certificate. Your operating system or browser already ships with a list of trusted roots. The client checks the signatures up the chain, that the dates are valid, and that the name in the certificate matches the host it wanted.

Useful extensions in the first message (ClientHello)

  • SNI (Server Name Indication): the client writes the host name in the first message. The server needs it to pick the right certificate before any HTTP is read, so it cannot wait for the Host header. It is virtual hosting, one layer lower.
  • ALPN: the client lists the application protocols it speaks (h2, http/1.1) and the server picks one. It costs no extra round trip. If the server does not know ALPN, the connection simply stays on HTTP/1.1.

How many round trips?

Here we count after the TCP handshake, with no session resumption. TLS 1.2 needs 2 round trips. TLS 1.3 needs only 1: the client guesses the key method and sends its part (key_share) already in the first message, so the server can answer with everything at once.

TLS 1.3                           TLS 1.2
 client                  server    client                  server
   |-- ClientHello + key -->|        |-- ClientHello ------->|
   |<-- ServerHello, cert,  |        |<-- ServerHello, cert -|   round trip 1
   |    Finished -----------|        |-- key exchange ------>|
   |-- Finished + data ---->|        |<-- Finished ----------|   round trip 2
   |   (1 round trip)       |        |-- data -------------->|
TLS 1.3 saves one whole round trip.

Forward secrecy and 0-RTT

Imagine a spy records your encrypted talk for months and later steals the server's private key. With forward secrecy the old talks stay safe, because every connection used a fresh, throwaway Diffie-Hellman pair that was deleted, and the certificate key only signs. Old TLS 1.2 with static RSA key exchange did not have this: stolen key, all recordings readable. TLS 1.3 removed static RSA and made forward secrecy mandatory.

When you reconnect, TLS 1.3 can use a saved secret (a PSK, from a ticket) and send data in the very first flight. This is 0-RTT or early data. It is faster, but the server cannot add a fresh value, so an attacker can replay (resend) that first flight. Early data also lacks forward secrecy. So it should only carry requests that are safe to repeat.

↑ Back to the contents

HTTP/2: many requests on one connection

HTTP/1.1 has a queue problem: one connection carries one request at a time. To go faster, browsers open about six connections per origin. A page with 200 small images then needs 34 rounds of six, and each round costs a round trip. With 50 ms of delay that adds up to seconds.

Frames, streams and messages

HTTP/2 keeps the same meaning (methods, status codes, headers) and changes only how it is sent. It is binary, not text. The smallest piece is the frame, with a fixed 9-byte header that has length, type, flags and a stream number. A stream is a two-way logical channel inside the connection. A message (a request or a response) is the group of frames on one stream. Frames from different streams are mixed together, which is called multiplexing.

one TCP connection:
 [S1 HEADERS][S3 HEADERS][S1 DATA][S5 HEADERS][S3 DATA][S1 DATA]...
    ^ stream 1     ^ stream 3 ... frames of many streams interleaved
Many requests share one connection.
  • Stream numbers are 31-bit. The client uses odd numbers, the server uses even ones, and 0 is for the connection itself. They only grow and are never reused.
  • HTTP/2 has no request line or status line. They become pseudo-headers that start with a colon: :method, :scheme, :authority (replaces Host), :path, and :status in the response.
  • Frame types: HEADERS and DATA carry the message. RST_STREAM cancels one stream (in HTTP/1.1 you had to close the connection). GOAWAY shuts the connection down. SETTINGS, PING and WINDOW_UPDATE handle settings, liveness and flow control.
  • You start HTTP/2 over TLS using ALPN with the id h2. h2c is HTTP/2 without TLS, used between internal services, not by browsers.

HPACK: shrinking headers

Headers repeat a lot: the same user-agent and cookie are sent in every request. HTTP/2 compresses them with HPACK. It keeps two tables. The static table has 61 fixed entries for very common headers, like :method: GET. The dynamic table starts empty and fills with the headers seen on this connection (the oldest are evicted when it is full). Both sides keep the same table, so a header seen before costs only a small index number.

Why not just use gzip, as the older SPDY did? Because compressing a secret (like a cookie) together with text the attacker controls leaks information through the size. This is the CRIME attack. HPACK never searches for partial repeats inside a value: it replaces a whole header by an index or uses a fixed , and it can mark sensitive fields as 'never indexed'.

Flow control

Flow control stops a fast sender from drowning a slow receiver. It applies only to DATA frames and works with two windows: one for each stream and one for the whole connection. Both start at 65,535 bytes. Each byte sent is subtracted from both. The receiver gives credit back with WINDOW_UPDATE. The sender may send at most the smaller of the two balances. (Priority is a different thing: only a hint about which stream to serve first.)

connection window: 65535
stream 1 sends 16384 + 16384 + 7232 = 40000 bytes
connection window left: 25535
stream 3 (never used) window: 65535
stream 3 may send: min(65535, 25535) = 25535 bytes
The smaller balance wins.

What did not work out

  • Server push: the server sent resources the client had not asked for yet. But the server does not know what the client already has cached, so it often wasted data. Browsers removed it. The replacement is 103 Early Hints and rel=preload, where the browser decides.
  • Domain sharding (splitting images over several host names, a trick for HTTP/1.1) now hurts: each new host costs a lookup, a TCP and TLS handshake and a new slow start, and it breaks the single warm connection and the single HPACK table.
  • Head-of-line blocking in TCP: HTTP/2 fixed the queue in the application, but all streams share one ordered TCP byte stream. If one packet is lost, TCP holds back everything after it, even frames of streams that lost nothing. On a lossy link, one HTTP/2 connection can be worse than six HTTP/1.1 connections.
Rounds needed to ask for 200 images
HTTP/1.1 (6 connections)
34 rounds
HTTP/2 (1 connection)
1 rounds
↑ Back to the contents

HTTP/3 and QUIC

To remove the problem of the single TCP queue, the Internet built a new transport called QUIC. It runs on top of UDP, but UDP is only an envelope that crosses the existing network. Everything else lives inside QUIC: streams, resending lost data, flow control, and encryption. Because QUIC usually runs in user space (inside the browser or server, not in the operating system), it can evolve with an app update. The cost: it uses more CPU per packet than kernel TCP. HTTP/3 is HTTP carried over QUIC.

FeatureHTTP/1.1HTTP/2HTTP/3
TransportTCPTCPQUIC over UDP
FormatTextBinary framesBinary frames
Requests at once on a connection1ManyMany
Header compressionnoneHPACKQPACK
Round trips before the first request (TLS 1.3, first visit)221
One lost packet delaysits connectionevery streamonly its own stream

Three big wins

  • No head-of-line blocking in the transport. Each QUIC stream is ordered by itself. When a packet is lost, only the streams whose data it carried wait. The rest keep arriving.
  • A shorter handshake. With TCP, first comes TCP (1 round trip) and then TLS 1.3 (1 round trip): 2 in total before the first HTTP request. QUIC carries the TLS 1.3 handshake inside its own first packets: 1. (Counted for a first visit, with no resumption and no TCP Fast Open. On a resumed connection both can use 0-RTT, with the replay risk you saw in the TLS section.)
  • Connection migration. A TCP connection is tied to four things: two IP addresses and two ports. If your phone moves from Wi-Fi to mobile data, the IP changes and TCP is lost. QUIC uses connection IDs carried in the packets, so the server checks the new path and goes on with the same keys and streams.
TCP + TLS 1.3                 QUIC
 client          server        client          server
  |--SYN--------->|             |--Initial+ClientHello->|
  |<--SYN/ACK-----|  1 RTT      |<--Initial+ServerHello-|  1 RTT
  |--ACK+Hello--->|             |--Finished + request-->|
  |<--ServerHello-|  2 RTT      |
  |--Finished+GET>|
Counting full round trips before the first request is sent.

There is no unencrypted QUIC: the TLS 1.3 handshake is part of connecting, and earlier TLS versions are not accepted. QUIC also encrypts almost all its own control information (packet numbers, acknowledgements). The reason is ossification: when boxes in the middle of the Internet get used to reading a protocol's format, they break any change, even a valid one. TCP suffered from this because its header is in the clear. QUIC leaves visible only the minimum, like the connection ID, so the network cannot depend on the rest.

QPACK, Alt-Svc and fallback

  • QPACK replaces HPACK. HPACK's table is changed by the header blocks themselves, so they must be decoded in exact order, but QUIC only orders data inside each stream. QPACK sends table updates on a separate stream, and each block says which entries it needs, so the decoder knows if it must wait.
  • Finding out that HTTP/3 exists. An https:// address does not say the version, so the browser starts with TCP. The server then announces alt-svc: h3=":443"; ma=86400 (protocol, UDP port, how long it is valid). Another way is a special DNS record of type HTTPS. Then the next connection can use QUIC.
  • It is only a hint. Some networks block UDP. The browser tries QUIC, and if it does not work it uses the TCP connection (HTTP/2 or HTTP/1.1), usually racing both so the user does not wait for a timeout.
Round trips before the first HTTP request (first visit, TLS 1.3 convention)
TCP + TLS 1.3 (HTTP/1.1 or HTTP/2)
2 round trips
QUIC (HTTP/3)
1 round trips

When is HTTP/3 best? When latency is high (the handshake saves a round trip) or when there is packet loss (streams do not block each other), as on mobile networks. On fast, clean networks HTTP/2 and HTTP/3 perform about the same, and user-space QUIC can even use more CPU.

↑ Back to the contents

REST: an API made of things

An API is a menu of things a program may ask of another program. REST is a style for building web APIs, described by Roy Fielding in his doctoral thesis (2000). Its main idea: do not think of actions, think of things (called resources), such as books, orders or users. Every thing has an address, a URL.

The URL is a noun, the HTTP method is the verb.
Want to...RequestAnswer
Read book 42GET /books/42200 OK and the book
Create a bookPOST /books201 Created with a Location header with the new URL
Replace book 42PUT /books/42 (the whole book)200 or 204
Change one fieldPATCH /books/42 (only the changes)200 or 204
Remove book 42DELETE /books/42204 No Content

Do not put the action in the path (/getBook?id=42). Doing so throws away what browsers, caches and proxies already know about the methods: for example, that GET is safe and cacheable.

Stateless

In REST the server keeps no conversation memory about the client. Each request carries everything needed to understand it (credentials, parameters), so any server copy can answer it. That helps to scale with many servers and to recover from failures. The cost: some data is repeated in each request. This is about session state; the server still stores resources like books normally.

Richardson maturity model

  1. Level 0: one address used as a tunnel for everything.
  2. Level 1: separate resources, each with its own URL.
  3. Level 2: proper HTTP methods and status codes.
  4. Level 3: hypermedia controls (HATEOAS): the answer tells the client what it can do next, with links.

Picking the right status

400 for a malformed request, 404 when the target does not exist, 409 when a valid request clashes with the current state (for example, a second book with an ISBN that must be unique), and 422 when the format is right but the content breaks a rule. Precise codes let clients react without reading the error text.

Retries: DELETE and Idempotency-Key

After a timeout the client does not know if the server finished the job. DELETE is safe to repeat: the second one may return 404, and that does not break idempotency, because the final state is the same. POST is not idempotent, and repeating it can create two orders. The fix is an idempotency key: the client invents a unique value (Idempotency-Key: abc123) and repeats it on every retry. The server recognises the repeat and returns the result of the first run. This header is a common convention, not part of core HTTP.

PUT or PATCH?

PUT replaces the state with the representation you send, so you must send the full resource, and it is idempotent. PATCH sends only the changes, which saves bytes and avoids overwriting fields other people changed, but by definition it is not idempotent.

Pagination

Big lists are returned in pages. Offset pagination says 'skip 3, give me 3'. If two new items are added at the top between two page requests, the window shifts and you receive items you already saw. Cursor pagination says 'give me 3 items after id 8', so it continues from the last item you saw and stays stable, but you cannot jump to page 50 directly.

newest first:  12 11 10 9 8 7 6 5 4 3 2 1     (12 and 11 added after page 1)
 page 1 was:  10 9 8
 offset=3 :   skips 12 11 10  -> returns 9 8 7   (9 and 8 repeated)
 before=8 :   id < 8          -> returns 7 6 5   (nothing repeated)
Why cursors are more stable.

Over-fetching and under-fetching

In REST the server decides the shape of each answer. If it gives more than the screen needs, bytes are wasted: over-fetching. If it gives less, the client must make more requests: under-fetching. Example: a screen with an author, their 5 books and each book's publisher (5 different ones) needs 1 + 5 + 5 = 11 requests with endpoints that return only ids. Fixes: composite endpoints, an expand parameter, or GraphQL.

↑ Back to the contents

GraphQL: ask for exactly what you want

GraphQL has one address and a typed schema, a list of all the types and fields that exist. The client sends a query that lists the exact fields it wants, even through relations, and the answer has the same shape. One request can replace the several requests of REST.

query {
	book(id: 7) {
		title
		author { name }
	}
}
The client chooses the fields (a selection set). The answer mirrors this shape.

The schema and the root types

  • Every type is nullable by default. A ! makes it non-null: title: String! means the server promises never to return null there (on an argument, the client must supply a value).
  • A schema has up to three root operation types: Query (read), Mutation (write, then read) and Subscription (a long stream of events). Only Query is required. Their fields are the entry points of the API.

Resolvers

The server runs the query field by field. For each field it calls a function called a resolver, giving it the parent object and the arguments. The schema describes the shape of the data; the resolvers say where the data comes from.

Errors and null

An answer is an object with data and, if something failed, errors. Because an operation can succeed partly, the usual convention (JSON over HTTP) answers `200` even with errors, so the client must always read errors, not only the HTTP code.

When a resolver fails, its field becomes null. If the field is ! (non-null), null is not allowed, so the null propagates up to the parent, and again, until the first nullable field. If none is found, the whole data is null. So marking everything with ! has a cost: one small failure can erase a big part of the answer.

type User { name: String!  posts: [Post!]! }
type Post { title: String! }

the title of post 2 fails:
  title  is String!   -> post 2 cannot be null
  [Post!]!            -> the list cannot hold a null item, so it cannot be null
  User.posts is !     -> posts cannot be null
  user(id) is nullable-> user becomes null

result:  { "data": { "user": null }, "errors": [ ... ] }
Null climbs until it finds a field that is allowed to be null.

Variables, aliases and execution order

  • Variables ($term) keep the structure of the query separate from the data. The values travel in a JSON map next to the document and are checked against the declared type. If you glued the text into the query instead, a user could inject pieces of query. Variables also let the same document be cached or registered as a persisted query.
  • Aliases (first: user(id: 1) and second: user(id: 2)) change the key in the answer. Without them, asking for the same field twice with different arguments is rejected, because both would use the key user.
  • Order. Query fields may run in any order, even in parallel, because they have no side effects. The top-level fields of a mutation run one after the other, in the order written. This is only for the top level; subfields run normally.

The N+1 problem

Because each field has its own resolver, a list of N items with a nested field makes 1 query for the list and then N queries for the nested field. With 20 posts and their authors, that is 1 + 20 = 21 SQL queries, even if there are only 4 different authors. The fix is a DataLoader: resolvers ask the loader for a key, the loader collects all the keys of the current step, removes repeats and runs one query (WHERE id IN (...)). Now it is 1 + 1 = 2. A loader lives for one request, so it never leaks rows to other clients.

SQL statements for a list of 100 books with authors and reviews (this repository's mini-project)
Without batching
201 statements
With a batching loader
3 statements

Cost, depth and caching

Since the client writes the query, the client also decides how expensive it is. A type that points to itself allows endless nesting, and cost multiplies: four levels of friends(first: 100) ask for 100 x 100 x 100 x 100 = 100^4 = 100 million objects. Counting requests is not enough, because one request can be abusive. Servers analyse the depth and an estimated complexity of the document and refuse it before running it.

Caching is harder than in REST. The HTTP cache is designed around GET and one URL per resource, but typical GraphQL sends everything with POST to /graphql, and the difference is in the body. Ways out: send queries by GET, use persisted queries identified by a hash in the URL, or cache on the client by type and id.

Size of a list answer in this repository's benchmark
REST (whole books)
27.6 kB
GraphQL (2 fields)
3 kB
↑ Back to the contents

JSON-RPC and gRPC: calling a function far away

An RPC (Remote Procedure Call) makes a call to another computer feel like calling a normal function. You do not think of resources or fields: you name an action (book.get, sum) and give it parameters.

JSON-RPC 2.0

A very small specification. A request has jsonrpc: "2.0", method, optional params and an id. The answer has jsonrpc, the same id (so you can match answers to requests) and exactly one of result or error, never both. An error has an integer code, a message and optional data.

{ "jsonrpc": "2.0", "method": "book.get", "params": { "id": 7 }, "id": 1 }
{ "jsonrpc": "2.0", "result": { "title": "Example" }, "id": 1 }
A request and its answer.
Error codeMeaning
-32700Parse error: the text is not valid JSON
-32600Invalid Request: it is JSON but not a valid request object
-32601Method not found
-32602Invalid params: for example "abc" where an integer is expected
-32603Internal error
-32000 to -32099Reserved for the server's own errors. Application errors use codes outside the reserved range
  • Notification: a request without `id`. The server must not answer at all, not even an error. Good for events and logs, but the client never knows if it failed.
  • Batch: an array of requests sent together. The server returns an array with one answer for each request that has an id, in any order. Notifications get nothing. An error in one element does not affect the others. If the batch holds only notifications, the server returns nothing.
  • A batch only saves round trips for calls that do not depend on each other.
JSON-RPC is simple and independent of the transport, but loses what HTTP gives for free, like GET caching and status meanings.
TopicRESTJSON-RPC
Remove book 42DELETE /books/42POST /rpc with {"method":"book.delete","params":{"id":42},"id":1}
The action is inthe HTTP methodthe body (method)
The outcome is inthe status coderesult or error in the body
TransportHTTPany (HTTP, sockets...)

gRPC

gRPC starts from a contract written in Protocol Buffers (a .proto file). The same file defines the services and messages and the compact binary encoding, where each field is identified by a number instead of a name. A compiler writes typed code in many languages, so calling the remote service looks like calling a local function. gRPC travels over HTTP/2: each call is a stream (headers with the method name, messages in DATA frames, and a final status in trailers), and multiplexing lets many calls share one connection.

message Book {
	string title = 1;
	int32 year = 2;
}
service Library {
	rpc Get (BookId) returns (Book);                 // unary
	rpc Upload (stream Chunk) returns (Summary);     // client streaming
}
The numbers 1 and 2 are what travels on the wire.
Call typeMessagesSignature
Unary1 request, 1 responserpc A (Req) returns (Res)
Server streaming1 request, many responsesrpc A (Req) returns (stream Res)
Client streamingmany requests, 1 responserpc A (stream Req) returns (Res)
Bidirectionalmany both ways, independentrpc A (stream Req) returns (stream Res)

Deadlines. The client says until when it is willing to wait. If time runs out, the call ends with DEADLINE_EXCEEDED and the server is told to stop working. In a chain A to B to C the deadline is passed along as the remaining time. If A gives 500 ms and B uses 200 ms, C gets 300 ms. If C needs 400 ms, the call ends at the 500 ms mark with DEADLINE_EXCEEDED.

t=0     A calls B (deadline = t 500)
t=200   B calls C  (300 ms left)
t=500   deadline hits -> DEADLINE_EXCEEDED, C is cancelled
        (C would have finished at t 600)
Nobody keeps working for someone who has stopped waiting.
Round trips to read a book with its author (this repository's mini-project)
REST
3 round trips
JSON-RPC
2 round trips
GraphQL
1 round trips
↑ Back to the contents

WebSocket and server-sent events: the server talks first

In plain HTTP the client always asks first. But a chat, a game or a stock board needs the server to send news by itself. There are four ways to do it.

TechniqueHow it worksGood for
PollingThe client asks again every few secondsRare changes
Long pollingThe server holds the request open until it has news; one request per messageSimple notifications
Server-sent events (SSE)One HTTP response that never ends; one-way, server to clientDashboards, feeds, progress bars
WebSocketOne connection, two-way, text or binaryChat, games, collaboration

WebSocket

A WebSocket starts as an ordinary HTTP/1.1 GET that asks to upgrade. If the server agrees, it answers 101 Switching Protocols, and from then on the same TCP connection speaks the WebSocket protocol. ws:// runs over plain TCP and wss:// runs inside TLS (like http:// and https://).

GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: AAECAwQFBgcICQoLDA0ODw==

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: Bz3qJYTGdOe8gUSpLosEdiLKDrk=
The handshake: request, then 101.
  • Sec-WebSocket-Accept is a of the client's key plus a fixed public text, written in . It only proves that the server understood WebSocket and answered this request. It authenticates nobody and encrypts nothing. That is the job of TLS and of your app.
  • After the handshake, data moves in frames that are text (UTF-8) or binary. Three control frames exist: ping and pong (a heartbeat: 'are you there?' / 'yes') and close (start an orderly goodbye with a code and a reason).
  • Masking: every frame from the client is XORed with a random 32-bit key sent in the same frame. This does not hide data. It protects broken proxies from being tricked into treating the bytes as HTTP (cache poisoning), because the page's script is not trusted to choose the exact bytes. Server frames are not masked.

Server-sent events

SSE is just a normal HTTP response that does not end. The server uses Content-Type: text/event-stream and keeps writing text. The browser reads it through the EventSource API. To talk back, the client makes other ordinary HTTP requests.

: keep-alive            <- a comment (starts with a colon)
retry: 5000            <- reconnect after 5 seconds

id: 42
event: price
data: 11

An event is a block of lines ended by an empty line.
  • Fields: data (the content), event (the name; without it the name is message), id (the position in the stream) and retry.
  • An event is delivered only when the empty line that closes its block arrives. Several data lines in the same block become one value joined with line breaks. A block that is not yet closed has not been delivered.
  • Without event: the handler onmessage is called. With event: update only listeners added with addEventListener('update', ...) are called.
  • If the connection drops, EventSource waits for the retry time and asks again, sending `Last-Event-ID` with the last id it saw. The server can resume without losing events.
Watch the video: Hussein Nasser: videos on HTTP, TLS, WebSocket, gRPC and their trade-offs↑ Back to the contents

Putting it all together

Now we can see how the pieces stack. When you open https://example.test for the first time, your browser starts a TCP connection, does a TLS 1.3 handshake (SNI picks the certificate, ALPN picks h2), then sends HTTP/2 requests. The answers carry Cache-Control so next time it is faster. The server may add alt-svc so that later you use HTTP/3.

NeedChooseWhy
Public, long-lived API with resources and HTTP cachingRESTStandard tools and GET caching
Many screens, each needing a different slice of connected dataGraphQLOne request, only the fields wanted. Needs batching and cost limits
Actions that are not resources, or internal servicesJSON-RPCSimplest to build, independent of transport
Fast service-to-service calls with a strict contractgRPCBinary, typed, HTTP/2 streaming and deadlines
Server only pushes updatesSSEPlain HTTP, auto-reconnect
Both sides send oftenWebSocketTwo-way, low overhead per message
LayerQuestion it answersKey ideas
HTTP semanticsWhat does the message mean?Methods (safe, idempotent), status codes, headers, framing
Caching, cookies, CORSWhat may be reused, remembered or read?max-age, no-store, revalidation, Vary, SameSite, origin
TLSIs it private and is it the right server?Certificates, key agreement, 1 vs 2 round trips, forward secrecy
HTTP/1.1, 2, 3How are messages put on the wire?Multiplexing, HPACK/QPACK, QUIC, head-of-line blocking
API stylesWhat does the client name and who shapes the answer?Resource, fields, procedure

Common mistakes

  • Thinking no-cache forbids storing (it means 'check first').
  • Mixing 401 (who are you?) with 403 (you may not).
  • Believing CORS protects the server (it protects the reading of the answer, in browsers).
  • Thinking GraphQL errors use 4xx or 5xx codes (the convention is 200 with errors).
  • Thinking HTTP/3 is always faster (it helps most with high latency or loss).
  • Forgetting that WebSocket needs an Origin check on the server.
  • Reusing a Protocol Buffers field number.
↑ Back to the contents