What is it? #
A request is a piece of text with four parts: a method, a path, headers and an optional body. A response has a status code, headers and usually a body.
That is the whole protocol at the surface level. Everything else — authentication, caching, compression, content types — is expressed through headers.
HTTP is stateless. Each request arrives with no memory of the previous one. If the server needs to know who you are, the request must carry that information, usually in a cookie or an Authorization header.
Statelessness is what makes scaling possible. Any server can handle any request, because no server holds the conversation in memory.
Think of it like this #
Sending a letter. The envelope carries the address and the postmark (headers), the letter inside is the content (body), and the type of service — ordinary, registered, return receipt — is the method.
The postal system does not remember your last letter. If this one needs context, you write it in.
Simple example #
A browser requests a product page while logged in. The request carries a cookie identifying the session; the response carries the HTML, caching instructions and a content type.
Code #
REQUEST
GET /api/products/42?currency=INR HTTP/1.1
Host: shop.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOi...
User-Agent: Mozilla/5.0
Accept-Encoding: gzip
(no body for a GET)
RESPONSE
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 87
Cache-Control: public, max-age=300
ETag: "a1b2c3"
{"id": 42, "name": "Keyboard", "price": 2499, "currency": "INR"}
import requests
response = requests.post(
"https://api.example.com/orders",
json={"sku": "P-1", "qty": 2}, # becomes the body
headers={
"Authorization": "Bearer token123", # who is asking
"Idempotency-Key": "order-abc-123", # safe to retry
},
timeout=10,
)
print(response.status_code) # 201
print(response.headers["Content-Type"])
print(response.json()) # parsed body
print(response.elapsed.total_seconds())
Headers worth knowing
Content-Type what format the body is in
Accept what formats the client can handle
Authorization credentials
Cache-Control how long this may be cached, and by whom
ETag a version tag, so the client can ask "changed since this?"
Set-Cookie server asks the client to store something and send it back
X-Request-ID a trace identifier, useful across services
Retry-After how long to wait before trying again
How it works #
The first line of a request carries the method and path. The query string after ? passes parameters for filtering or options.
Headers are key-value metadata. Accept says what the client wants back; Content-Type on a response says what it actually got. Mismatches here cause a large share of integration bugs.
The body carries data. GET requests normally have none; POST and PUT usually do.
The response status code is the summary: 2xx worked, 3xx redirect, 4xx the client made a mistake, 5xx the server did.
Cache-Control: public, max-age=300 tells browsers and proxies they may reuse this response for five minutes. That single header can remove most of your traffic — or, set wrongly, serve stale prices for a day.
ETag enables conditional requests. The client sends the tag back next time, and the server can answer 304 Not Modified with no body at all.
The Idempotency-Key in the Python example is the practical answer to statelessness plus retries: if the same key arrives twice, the server should return the original result rather than creating a second order.
Real-world use #
Debugging web problems is mostly reading requests and responses. Browser developer tools and curl -v show you exactly what was sent and received, which resolves most "it works locally" mysteries.
Caching headers are one of the highest-value settings in web performance. Static assets get long cache lifetimes with versioned filenames; dynamic pages get short ones or none.
Statelessness explains why session data lives in a cookie, a token, or a shared store like Redis rather than in server memory. Keeping it in memory breaks the moment a second server appears.
Request IDs propagated through headers are what make distributed tracing possible: one identifier follows a request through every service, so logs can be stitched together.
Common mistakes #
- Storing session state in server memory, which breaks with more than one server.
- Setting long cache lifetimes on responses that contain user-specific data.
- Ignoring the status code and parsing the body regardless.
- Sending JSON without a
Content-Type: application/jsonheader. - Retrying non-idempotent requests without an idempotency key.
Practice #
Use curl -v against a public API and identify the method, path, three request headers and three response headers. Then send the same request with Accept: text/html and compare. Finally, make a request twice and check whether any caching headers changed the second response.