HTTP for backend engineers
1 September 2026 · 5 min read · #backend #http #cors
written from this video
Nobody reads RFC 9110. What backend engineers actually need is the handful of HTTP mechanics that explain the network tab when something breaks: why the request was blocked, why the server returned 304 with no body, why the response is 26 MB instead of 3.8 MB. That's maybe six concepts, and they all fall out of two properties of the protocol.
The first is that HTTP is stateless. The server has no memory of your last request. Every request carries everything needed to process it — the method, the path, the credentials, the content type — and once the response is written the server forgets you exist. That's why you send the token on every single call rather than logging in once. It's also why horizontal scaling works at all: any of your six instances can serve any request, and if one crashes mid-traffic nothing about the client's state was living inside it.
The second is that communication is always client-initiated. The server waits. It never speaks first. Every "server push" thing you've used — SSE, websockets, long polling — is a workaround built on top of a connection the client opened.
Everything below is a consequence of those two.
Headers are the shipping label
A request is a method, a path, a version, some headers, a blank line, and a body. Same for the response, with a status code where the method was.
The headers are the part worth understanding, and the reason they're a separate section from the body is the same reason the address goes on the outside of the parcel. The courier needs to route the package without opening it. Proxies, CDNs, load balancers and browsers all need to make decisions about a message — cache it, block it, decompress it, refuse it — without parsing the body, and often before the body has even arrived.
That split gives you two things. Headers are extensible: you can add X-Request-Id tomorrow and no proxy on the path breaks, because nothing about the protocol itself changed. And they act as a remote control — the client sends Accept: application/xml and gets XML back, sends Accept-Language: es and gets Spanish, sends Authorization and gets a different access decision. Same route, same handler, different response, all driven from outside the body.
Headers are also where every "why is this working in Postman but not in the browser" bug lives. Postman is not a browser and enforces none of this.
CORS is a browser rule, not a server rule
This is the one that costs people afternoons, so it's worth being precise: your server did not block the request. The server processed it and answered. The browser then read the response, didn't find permission to hand it to your JavaScript, and threw it away. The request happened. The side effects happened. You just don't get to see the result.
A request is cross-origin if scheme, host or port differ — localhost:5173 calling localhost:3000 counts. From there the browser picks one of two flows.
Simple request. Method is GET, POST or HEAD, no unusual headers, boring content type. The browser attaches Origin, sends it, and checks the response for Access-Control-Allow-Origin matching your origin (or *). Present, you get the data. Absent, CORS error in the console.
Preflight. If the method is PUT or DELETE, or there's a non-simple header like Authorization, or the content type is application/json — the browser sends an OPTIONS request first, asking permission:
OPTIONS /api/resource HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
Note the third condition. Content-Type: application/json is not a simple content type, which means essentially every API call you write is preflighted. Two round trips for one request, by default.
The server answers 204 with no body and four headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age. Only if all the boxes tick does the real request get sent.
That last header is the one people leave at zero and forget. Access-Control-Max-Age: 86400 tells the browser to cache the preflight answer for a day, so you stop paying the extra round trip on every call. Set it to 0 while debugging, set it high in production.
ETags: 304 is a success
HTTP has caching built in, and it's three headers on the way out and two on the way back.
The server responds with Cache-Control: max-age=10, an ETag (a hash of the response), and Last-Modified. Next time, the client sends those values back as If-None-Match and If-Modified-Since — effectively asking "still this one?" If nothing changed, the server replies 304 Not Modified with no body. The client renders its cached copy. You saved the entire payload and paid for one round trip of headers.
The failure mode is obvious once you've been bitten: the ETag is yours to maintain. Update the resource, forget to change the hash, and every client on earth keeps serving stale data with the server's blessing. This is why client-side caching layers like React Query largely displaced HTTP caching for API data — the client decides what's stale, and there's no hash to forget. HTTP caching still wins for static assets, where the content and the hash are generated together.
Compression and streaming are one line each
Accept-Encoding: gzip in, Content-Encoding: gzip out. Same 11,000-record JSON file: 3.8 MB compressed, 26 MB not. That is the whole feature, and it is usually one flag in your framework that someone forgot to turn on.
For big payloads in the other direction, Content-Type: multipart/form-data sends binary in parts separated by a boundary string, so nothing has to buffer the whole file. For big responses, Content-Type: text/event-stream plus Connection: keep-alive keeps the socket open and appends chunks as they arrive, instead of making the client wait for the last byte before rendering the first.
Persistent connections have been the default since HTTP/1.1, so Connection: keep-alive mostly shows up in the network tab as confirmation rather than as something you configure. Same for TLS: certificates authenticate the server, everything in transit is encrypted, HTTPS is HTTP over that. Below the application layer it becomes network engineering, and you can stop reading.