These are my own study notes, turned into a guide. I wrote it after learning how HTTP really works under the hood. If you are a backend engineer, or you want to become one, this covers the small part of HTTP that explains most of the bugs you will debug in production.
Table of Contents
- The Two Main Ideas Behind HTTP
- Network Layers and the History of HTTP
- The Structure of an HTTP Message
- HTTP Methods and Idempotency
- CORS: The Browser's Security Guard
- HTTP Status Codes
- HTTP Caching
- Content Negotiation and Compression
- Persistent Connections and Large Payloads
- TLS, SSL, and HTTPS
The Two Main Ideas Behind HTTP
HTTP is the language of the web. Every time a browser talks to a server, it uses HTTP. The whole protocol is built on two simple ideas: it is stateless, and it follows the client-server model.
Statelessness
HTTP has no memory of past requests. Every request is self-contained. It carries everything the server needs to handle it: headers, URL, method, and body. Once the server sends a response back, it forgets the request happened. If the same client sends another request one second later, the server treats it as something completely new, with no link to what came before.
This is why every request needs to carry proof of who is sending it, like an auth token or a session cookie. The server does not remember you from one request to the next, so you have to remind it every time.
Why is this actually good design?
- Simplicity: the server does not need to store session data in memory. This keeps the server code simpler.
- Scalability: you can spread requests across many servers with no problem, because no single server has to track a client's session. If one server crashes, the client is not affected, because there was no state to lose in the first place.
Since HTTP has no memory on its own, developers built techniques on top of it, like cookies, sessions, and tokens, to keep some kind of continuity for things like logins or shopping carts.
The Client-Server Model
In any HTTP flow, there is always a client and a server:
- Client: usually a browser or an app. It always starts the connection and sends the request.
- Server: hosts the resources, like websites or APIs, and waits for requests. When a request arrives, the server processes it and sends back a response, such as HTML, JSON, or a plain text file.
HTTP and HTTPS follow the same principles. HTTPS is just HTTP with TLS encryption on top for extra security.
Good questions for interviews or exams:
- What is statelessness in HTTP, and what are its benefits?
- How do we work around HTTP being stateless?
- Who always starts the connection in the client-server model?
Network Layers and the History of HTTP
Before a client and server can talk over HTTP, they need a path between them. HTTP needs a reliable transport, one that does not lose messages. That is why it runs on top of TCP, which is connection-based, instead of UDP.
In the OSI model, HTTP lives at Layer 7, the Application Layer. As backend engineers, this is mostly where our work happens. We do not need to go deep into TCP handshakes or the lower layers. That part is network engineering.
How HTTP Changed Over the Years
- HTTP/1.0: opened a new TCP connection for every request, then closed it. This was slow and wasted resources.
- HTTP/1.1: added persistent connections. One TCP connection could now handle many requests and responses. It also added caching and chunked transfer encoding.
- HTTP/2: added multiplexing, so many requests and responses can travel at the same time over one connection. It moved from plain text to binary framing, and added header compression, called HPACK, plus server push.
- HTTP/3: built on the QUIC protocol, which runs over UDP instead of TCP. This gives a faster connection start, lower latency, and fixes a problem called head of line blocking that still existed in HTTP/2.
Good questions for interviews or exams:
- What is the main difference between HTTP/1.0 and HTTP/1.1?
- What are the main additions in HTTP/2?
- Which OSI layer does HTTP work in?
The Structure of an HTTP Message
A request has
- Method (GET, POST, and so on)
- Resource URL (the path you are asking for)
-
HTTP version (like
HTTP/1.1) - Host (the domain)
- Headers
- A blank line that separates the headers from the body
- Body (only if needed)
A response has
- HTTP version
-
Status code (like
200) -
Status text (like
OK) - Headers
- A blank line
- Body
Headers: Extra Information About the Message
Headers are key-value pairs that carry extra information about the request or the response. Think about sending a parcel. You write the name and address of the receiver on the outside of the box, not inside it. This lets the shipping company read the information fast, without opening the box. Headers work the same way. They let the client and server share information without touching the actual data, which is the body.
1. Request Headers
These are sent by the client to tell the server who it is, what it wants, and what format it understands.
| Header | Purpose | Example |
|---|---|---|
User-Agent |
Tells the server what client is making the request | Mozilla/5.0 |
Authorization |
Carries login credentials | Bearer abc123token |
Accept |
Tells the server what format the client wants back | application/json |
Accept-Language |
The client's preferred language | en-US,ar-EG |
Accept-Encoding |
What compression the client supports | gzip, br |
GET /users HTTP/1.1
Host: example.com
Accept: application/json
Authorization: Bearer token123
This request means: "send me JSON, and here is my token to prove I am logged in."
2. General Headers
These carry information about the message itself, and they are not specific to just the request or just the response.
| Header | Purpose | Example |
|---|---|---|
Date |
When the message was sent | Date: Fri, 05 Jun 2026 01:00:00 GMT |
Cache-Control |
How the response should be cached | Cache-Control: max-age=3600 |
Connection |
Whether the connection should stay open | Connection: keep-alive |
3. Representation Headers
These describe the shape of the data inside the body.
| Header | Purpose | Example |
|---|---|---|
Content-Type |
The format of the body |
application/json, text/html, image/png
|
Content-Length |
The size of the body in bytes | 245 |
Content-Encoding |
Whether the body is compressed | gzip |
ETag |
A fingerprint of the current version of the resource | "v1.7-abc" |
ETag is technically a representation header, but in real use it works more like a cache validator than just a description of the content. You will see it a lot in the caching section below.
4. Security Headers
These help protect the connection from attacks.
| Header | Purpose |
|---|---|
Strict-Transport-Security (HSTS) |
Forces the browser to only use HTTPS, so it cannot be tricked into using plain HTTP |
Content-Security-Policy (CSP) |
Limits where scripts, styles, and images can load from, which reduces XSS attacks |
X-Frame-Options |
Stops the page from being loaded inside an iframe, which prevents clickjacking |
Set-Cookie with HttpOnly / Secure / SameSite
|
Stops JavaScript from reading the cookie, forces it to be sent only over HTTPS, and reduces CSRF |
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
Two more ideas worth remembering
-
Extensibility: HTTP lets you add custom headers, often with an
X-prefix, without breaking the protocol. The server can read them if it understands them, or just ignore them. -
Remote control: headers work like a remote control for the server. Content negotiation (
Accept), caching (Cache-Control), and authentication (Authorization) all guide server behavior without changing the URL or the body.
Good questions for interviews or exams:
- Why do we put metadata in headers instead of the body?
- Give an example of a security header and explain what it does.
- What are representation headers? Give some examples.
HTTP Methods and Idempotency
Methods show the client's intent:
| Method | Intent |
|---|---|
GET |
Get data, do not change anything |
POST |
Create new data, comes with a body |
PATCH |
Update part of a resource |
PUT |
Replace the whole resource |
DELETE |
Remove a resource |
Best practice: many developers use PUT when they actually mean PATCH, and this is a mistake. A simple rule: always use PATCH for updates, unless you have a real reason to replace the whole resource. Only then use PUT.
Idempotent vs Non-Idempotent
Idempotent means: if you run the same request many times, the result stays the same and does not break the data.
-
GET: fetching the data 10 times gives you the same data every time. -
PUT: replacing a resource with the same data 10 times still ends with the same final state. -
DELETE: deleting something once removes it. Deleting it again does nothing new.
Non-idempotent means the result changes with each call. POST is the main example. If you click "create note" twice, you get two different notes.
There is also OPTIONS. The client uses this not to get data, but to ask the server what it can do. This is the method behind CORS preflight requests, which we cover next.
Good questions for interviews or exams:
- What is the main difference between
PUTandPATCH, and why isPATCHthe best practice? - Explain idempotency, with examples of idempotent and non-idempotent methods.
CORS: The Browser's Security Guard
Browsers follow a Same-Origin Policy. A frontend running on one domain is blocked from freely calling a backend on a different domain, for security reasons. CORS (Cross-Origin Resource Sharing) is the mechanism that lets a server allow specific origins to bypass this rule.
There are two request flows:
1. Simple Request
A request counts as "simple" if its method is GET, POST, or HEAD, and it does not carry complex headers.
The browser adds an Origin header on its own. If the server replies with Access-Control-Allow-Origin that matches this origin, or with *, the browser lets your JavaScript see the response. If not, it blocks it and shows a CORS error in the console.
2. Preflight Request
The browser sends a preflight request before the real one if any of these are true:
- The method is not "simple" (like
PUTorDELETE). - The request has non-simple headers, like
Authorizationor a custom header. - The
Content-Typeis not simple. Most JSON APIs fall into this case, sinceapplication/jsontriggers a preflight.
The preflight OPTIONS request has no body. It is just a question: "can I send you a PUT? Can I include Authorization?" If the server agrees, it replies with 204 No Content, plus the allowed origins, methods, and headers, and Access-Control-Max-Age so the browser can save this approval instead of repeating the preflight on every single request.
Good questions for interviews or exams:
- When does the browser decide to send a preflight instead of a simple request?
- What is the
OPTIONSmethod and what is its role in CORS? - What does
Access-Control-Max-Agedo in the preflight response?
HTTP Status Codes
Status codes give the client a standard way to understand the result of a request, without needing to read the response body. They are grouped by their first digit:
1xx: Informational
-
100 Continue: the server got the headers and the client can now send the body (useful for large uploads). -
101 Switching Protocols: used when moving from HTTP to WebSocket.
2xx: Success
-
200 OK: the request worked (usually withGET). -
201 Created: the request worked and created a new resource (usually withPOST). -
204 No Content: the request worked but there is no body to return (common withOPTIONSorDELETE).
3xx: Redirection
-
301 Moved Permanently: the resource moved for good. For example, if you rename/userto/person, you return301so old clients still work and nothing breaks for them. -
302 Found: a temporary redirect. The client should still use the original path in the future. -
304 Not Modified: the resource did not change, so the client can use its cached copy (used together withETag).
4xx: Client Errors
-
400 Bad Request: the client sent bad data. -
401 Unauthorized: no auth token was sent, or the token expired. -
403 Forbidden: the token is valid, but the client does not have permission for this action. -
404 Not Found: the resource or path does not exist. -
405 Method Not Allowed: using a method that this endpoint does not support. -
409 Conflict: a conflict in the data, like trying to create something that already exists. -
429 Too Many Requests: the client went over a rate limit.
5xx: Server Errors
-
500 Internal Server Error: something went wrong on the server and was not handled properly. -
501 Not Implemented: this feature does not exist yet, but it might be added later. -
502 Bad Gateway: seen with proxies like Nginx, when the main server sends back an invalid response. -
503 Service Unavailable: the server is down for a short time, because of high traffic or maintenance. -
504 Gateway Timeout: the proxy got no response from the main server in time.
401 vs 403, in one line:
401means "I do not know who you are."403means "I know exactly who you are, but you are not allowed in."
Good questions for interviews or exams:
- What is the difference between
401 Unauthorizedand403 Forbidden? - When do you use
409 Conflict? - What is the difference between
301and302redirects?
HTTP Caching
Caching means saving copies of responses, so we do not have to ask the server for them again. This lowers load time and saves bandwidth.
How ETag-based caching works
- First
GET: the server responds with200, plusCache-Control: max-age=10, anETag(a fingerprint of the response), andLast-Modified. - Second
GETfor the same resource: the browser automatically addsIf-None-Match(the oldETag) andIf-Modified-Since(the old date). This basically asks the server: "is this ETag still current, or did the data change since this date?" - If nothing changed, the server responds with
304 Not Modified. This tells the browser it can safely use its own cached copy. - Once the resource gets updated, for example through
POSTorPATCH, the server generates a newETag. On the nextGET, the browser still sends the oldETaginIf-None-Match, the server sees it is no longer valid, and responds with200 OK, the fresh data, and the newETag.
Best practice: relying only on plain HTTP caching with ETags in production can get complicated and puts extra load on the server to manage. A better option is often a client-side caching library, like React Query, which gives the client full control over when to fetch new data and when to reuse the cache. Still, it is worth knowing that the native HTTP caching option exists for simpler cases.
Good questions for interviews or exams:
- What is an
ETag, and how does it work withIf-None-Matchto enable caching? - When does the server respond with
304 Not Modified?
Content Negotiation and Compression
Content negotiation is how the client and server agree on the best format to exchange data. It has three main types:
-
Media type negotiation: through the
Acceptheader (likeapplication/jsonvsapplication/xml). -
Language negotiation: through
Accept-Language(like English vs Spanish). -
Encoding negotiation: through
Accept-Encoding(likegzipvsdeflate).
The client can ask for JSON in Spanish, then switch to XML in English, and the server adapts, without the client needing to change anything else about the request.
Compression
Since we are already talking about Accept-Encoding, compression deserves its own note. On a dataset with 11,000 entries, a response compressed with gzip came out to about 3.8 MB, while the same response without compression was about 26 MB. That is the real impact of compression. It shrinks payload size by a large amount, and this matters a lot when many clients are downloading the same large file. The client, meaning the browser, receives the compressed data and decompresses it on its own.
Good questions for interviews or exams:
- List the different types of content negotiation and their headers.
- Why do we use HTTP compression, and which header controls it?
Persistent Connections and Large Payloads
Keep-Alive
In HTTP/1.0, every request opened and closed a new TCP connection. This was slow and wasted resources. HTTP/1.1 made persistent connections the default, using the Connection: keep-alive header. This lets the client and server reuse the same connection for many requests, until one side decides to close it, which reduces latency and saves resources.
Uploading large payloads: multipart/form-data
When you upload binary data, like an image or a video, the request uses multipart/form-data. The key detail here is the boundary parameter. Since binary data is sent in parts, a delimiter string, called the boundary, marks where each part starts and ends, so the server can split them apart inside the request body.
Downloading large payloads: streaming
To send a large text response without freezing, the server streams it in chunks instead of sending one huge block. Two things make this work:
-
Content-Type: text/event-stream: tells the client to expect the data as a series of events. -
Connection: keep-alive: the connection needs to stay open until every chunk arrives.
The client keeps appending each chunk as it arrives, until the full file is complete.
Good questions for interviews or exams:
- What is the benefit of
Connection: keep-alive? - What is the
boundary, and why do we use it inmultipart/form-datarequests? - How does a server stream a large response to a client?
TLS, SSL, and HTTPS
You will not work with these directly most of the time as a backend engineer, but you still need to know what they mean.
- SSL (Secure Sockets Layer): the original protocol for encrypting data between client and server. It is now outdated and no longer used, because of known security weaknesses.
- TLS (Transport Layer Security): the modern replacement for SSL, and much more secure. It encrypts data while it travels, using certificates to confirm the server's identity and set up an encrypted connection. This stops eavesdropping and data leaks. The current recommended version is TLS 1.3.
- HTTPS: simply regular HTTP with TLS or SSL security added on top. It uses TLS as the encryption layer to protect login details and other sensitive data.
Good questions for interviews or exams:
- What is the difference between SSL and TLS, and why did we stop using SSL?
- What is HTTPS, and how does it work together with TLS?
Wrapping Up
Understanding this flow, from statelessness, to headers, methods, CORS, status codes, caching, negotiation, streaming, and TLS, is the key to debugging most of the problems you will meet as a backend engineer. You do not need to master TCP internals or the full TLS handshake to be effective. You need to picture this pipeline clearly enough to reason about where things break.
Bonus: A Few Code Examples
Some of these ideas are easier to understand with real code. Below are small examples in both C# (ASP.NET Core) and Go, covering idempotent updates, custom headers, CORS, and cache headers.
Example 1: PATCH (partial update) vs PUT (full replace)
C# (ASP.NET Core)
// PATCH: partial update, the recommended default
[HttpPatch("{id}")]
public async Task<IActionResult> PatchUser(int id, [FromBody] JsonPatchDocument<UserDto> patch)
{
var user = await _userService.GetByIdAsync(id);
if (user is null)
return NotFound();
patch.ApplyTo(user);
await _userService.UpdateAsync(user);
return NoContent(); // 204
}
// PUT: full replacement, only use it when you really mean it
[HttpPut("{id}")]
public async Task<IActionResult> ReplaceUser(int id, [FromBody] UserDto replacement)
{
var exists = await _userService.ExistsAsync(id);
if (!exists)
return NotFound();
await _userService.ReplaceAsync(id, replacement); // idempotent: same result every call
return NoContent();
}
Go
// PATCH: partial update
func PatchUser(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var patch UserPatch
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
if err := userService.ApplyPatch(id, patch); err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent) // 204
}
// PUT: full replacement, idempotent
func ReplaceUser(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var replacement User
if err := json.NewDecoder(r.Body).Decode(&replacement); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
userService.Replace(id, replacement)
w.WriteHeader(http.StatusNoContent)
}
Example 2: Enabling CORS for a specific origin
C# (ASP.NET Core)
// Program.cs
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("https://myfrontend.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials(); // needed if you send cookies or Authorization
});
});
var app = builder.Build();
app.UseCors("AllowFrontend");
Go
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "https://myfrontend.com" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "3600")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent) // 204 preflight response
return
}
next.ServeHTTP(w, r)
})
}
Example 3: ETag-based caching
C# (ASP.NET Core)
[HttpGet("{id}")]
public async Task<IActionResult> GetResource(int id)
{
var resource = await _resourceService.GetByIdAsync(id);
if (resource is null)
return NotFound();
var etag = $"\"{resource.Version}\"";
if (Request.Headers.TryGetValue("If-None-Match", out var clientEtag) && clientEtag == etag)
return StatusCode(StatusCodes.Status304NotModified);
Response.Headers.ETag = etag;
Response.Headers.CacheControl = "max-age=10";
return Ok(resource);
}
Go
func GetResource(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
resource, ok := resourceStore.Get(id)
if !ok {
http.NotFound(w, r)
return
}
etag := fmt.Sprintf(`"%s"`, resource.Version)
if match := r.Header.Get("If-None-Match"); match == etag {
w.WriteHeader(http.StatusNotModified) // 304
return
}
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "max-age=10")
json.NewEncoder(w).Encode(resource)
}







Top comments (0)