What HTTP is

HTTP (Hypertext Transfer Protocol) is the application-layer protocol a client uses to ask a server for a resource or to send data to it, and the server uses to answer. It carries web pages, images, API calls, software updates and file downloads. It follows the request-response model: one request, one response, and the client always speaks first. Over the public internet it almost always runs inside TLS, which makes it HTTPS (see HTTPS and TLS in Practice).

The current definition is split over a set of Request for Comments (RFC) documents from June 2022: RFC 9110 (HTTP Semantics: methods, status codes, headers), RFC 9111 (caching), RFC 9112 (HTTP/1.1), RFC 9113 (HTTP/2) and RFC 9114 (HTTP/3). They replace the older RFC 2616 and RFC 7230–7235, which you will still see quoted in older material. Because the semantics live in one document, a GET means the same thing in every version.

The anatomy of a message

An HTTP request is the message a client sends: a method, a target, headers and sometimes a body. An HTTP response is the server's answer: a status code, headers and usually a body. In HTTP/1.1 both are plain text with the same four parts:

  1. Start line: for a request, the method, the target and the version; for a response, the version, the status code and a reason phrase.
  2. Headers: one per line, Name: value. Names are case-insensitive.
  3. An empty line marks the end of the headers. Lines end in CR LF (carriage return, line feed).
  4. Body (optional): the content itself. Its length is given by Content-Length or by chunked transfer encoding.

Here a script on the Kestrel Energy customer portal reports a power cut to the outage API:

POST /v1/outages HTTP/1.1
Host: api.example.com
User-Agent: kestrel-portal/2.4
Accept: application/json
Content-Type: application/json
Content-Length: 50
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…

{"postcode": "2511 CV", "description": "No power"}
HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 62
Location: /v1/outages/4711

{"id": 4711, "postcode": "2511 CV", "status": "investigating"}

The request line says what (create something in the outages collection); the headers say how (the body is JSON, the client wants JSON back, here is the access token); the body carries the data. The response says it worked (201), where the new resource lives (Location) and returns it.

Methods

The HTTP method (sometimes called the verb) is the first word of a request and states what the client wants done with the target resource. The server decides which methods a resource supports; an unsupported one gets 405 Method Not Allowed with an Allow header listing the ones that work.

  • GET retrieves a representation of the resource. By far the most common method; every link and address bar visit is a GET.
  • HEAD is GET without the body: only the status and headers. Useful to check whether something exists or how big it is.
  • POST sends data for the resource to process: submit a form, create an item in a collection, start an action.
  • PUT replaces the resource at that URL with the body (or creates it there).
  • PATCH (RFC 5789) changes part of a resource.
  • DELETE removes the resource.
  • OPTIONS asks which methods and features a resource supports. Browsers send it as a CORS preflight (see Browsers and the Same-Origin Policy).
  • QUERY is new: published as RFC 10008 in June 2026, it is a read-only request that carries its query in the body, for searches too large or complex to fit in a URL. Expect support in servers, proxies and tools to arrive gradually.

Two more exist for special purposes: CONNECT opens a tunnel through a proxy, and TRACE echoes the request back (it is normally disabled).

Safe, idempotent, cacheable

RFC 9110 gives each method properties that clients, proxies and caches rely on. A safe method is one whose semantics are read-only: the client does not ask for any change on the server, so a browser may prefetch it and a crawler may follow it freely. (A server can still log the request; what matters is that the client did not request a change.) An idempotent method has the same intended effect whether it is sent once or ten times, so a client may safely retry it after a timeout. Cacheable means a response may be stored and reused (see HTTP Caching).

Method Safe Idempotent Cacheable Request body
GET yes yes yes no defined meaning; avoid
HEAD yes yes yes no defined meaning; avoid
OPTIONS yes yes no rarely
QUERY yes yes yes yes (the query)
PUT no yes no yes
DELETE no yes no no defined meaning; avoid
POST no no only with explicit freshness information (rare) yes
PATCH no no no yes

These properties are promises the server must keep. If GET /outages/4711/close closes an outage, a link preview, a prefetching browser or a search engine crawler can close it by accident. Changes belong in POST, PUT, PATCH or DELETE.

Status codes

An HTTP status code is the three-digit number at the start of a response that tells the client how the request went. The reason phrase after it ("Not Found") is only for humans; programs look at the number. The first digit gives the class:

  • 1xx informational: an interim answer, such as 101 Switching Protocols (used to start a WebSocket) or 103 Early Hints.
  • 2xx success: the request worked.
  • 3xx redirection: look elsewhere, or use your cached copy.
  • 4xx client error: the request was wrong or not allowed. Repeating it unchanged usually gives the same answer; the exceptions are 429 Too Many Requests and 408 Request Timeout, where waiting and retrying is the intended response.
  • 5xx server error: the request may have been fine, but the server, or something behind it, failed.

For troubleshooting, that first digit already splits the work: 4xx sends you to the request (URL, credentials, body), 5xx sends you to the server side (application, proxy, database).

Code Meaning What to check
200 OK Success; the body holds the result An API that returns 200 with an error message in the body is badly designed; read the body anyway
201 Created A new resource was created Location header points to it
204 No Content Success, deliberately no body Common answer to DELETE or PUT; not an error
301 Moved Permanently The resource has a new URL for good Browsers cache it hard. Clients may turn POST into GET; use 308 to keep the method
302 Found Temporarily elsewhere Same POST-to-GET caveat; 307 is the method-preserving version. A redirect loop shows as "too many redirects"
304 Not Modified Your cached copy is still valid Normal and good; see HTTP Caching
307 Temporary Redirect / 308 Permanent Redirect Like 302 / 301, but the method and body must be kept The safe choice for redirecting API calls
400 Bad Request The server cannot parse or accept the request Malformed JSON, missing parameter, bad header
401 Unauthorized Not authenticated: no valid credentials were sent Missing or expired token, wrong scheme; the WWW-Authenticate header says what is expected
403 Forbidden Authenticated (or not needed), but not allowed Permissions, roles, IP allow lists, a WAF rule
404 Not Found Nothing at this URL (or the server hides it) Typos, missing /v1, wrong host or environment
405 Method Not Allowed The URL exists, but not for this method The Allow header lists valid methods
409 Conflict The request conflicts with the current state Duplicate item, edit of an outdated version
422 Unprocessable Content Well-formed, but the content is invalid Validation errors: read the response body
429 Too Many Requests Rate limit reached (RFC 6585) Slow down; honor Retry-After if present
500 Internal Server Error Unhandled error in the application Application logs, recent deployments
502 Bad Gateway A proxy got an invalid or no answer from the upstream Is the application behind the proxy running?
503 Service Unavailable Overloaded or in maintenance Capacity, health checks, Retry-After
504 Gateway Timeout A proxy waited too long for the upstream Slow queries or calls behind the application; proxy timeouts

Headers

An HTTP header (formally a header field) is a name-value pair in a request or response that carries metadata about the message: what the body is, who is asking, how to cache it. Clients and servers ignore headers they do not understand, which is how HTTP has grown for thirty years without breaking. The ones you will meet every day:

Header Sent in Purpose
Host request Which site is meant. Required in HTTP/1.1; lets one IP address serve many sites (virtual hosts)
User-Agent request Which client is calling; useful in logs, easy to fake
Accept request Which media types the client wants back (see content negotiation)
Content-Type both The media type of the body, e.g. application/json
Content-Length both Size of the body in bytes
Location response Target of a redirect, or the URL of a created resource
Authorization request Credentials, e.g. Bearer <token>; never log it
WWW-Authenticate response With a 401: which authentication scheme to use
Cache-Control both Whether and how long the response may be cached (see HTTP Caching)
Set-Cookie / Cookie response / request State across requests (see Cookies, Sessions and State)

You can see the headers of any request with curl -I (a HEAD request) or curl -v:

curl -I https://www.example.com/outages
HTTP/2 200
content-type: text/html; charset=utf-8
content-length: 5120
cache-control: no-cache

Notice the lowercase names: curl negotiated HTTP/2, where header names are always sent in lowercase. Because names are case-insensitive, code must never compare them with an exact string match.

Example: status codes up close

Python's http.client is a low-level client: it shows the status line and headers exactly as received and does not follow redirects, which makes it ideal for seeing what a server really says. The script starts a small outage API on your own machine with a few routes that answer differently, then sends one request to each over a single persistent connection.

import http.client
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"  # the default is HTTP/1.0

    def reply(self, status, body=b"", headers=()):
        self.send_response(status)
        for name, value in headers:
            self.send_header(name, value)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        if self.command != "HEAD":
            self.wfile.write(body)

    def do_GET(self):
        if self.path == "/outages":
            self.reply(200, b'{"outages": []}',
                       [("Content-Type", "application/json")])
        elif self.path == "/outage-list":
            self.reply(301, headers=[("Location", "/outages")])
        elif self.path == "/admin":
            self.reply(401, b"Log in first\n",
                       [("Content-Type", "text/plain"),
                        ("WWW-Authenticate", 'Bearer realm="outage-api"')])
        elif self.path == "/report":
            self.reply(500, b"Internal error\n", [("Content-Type", "text/plain")])
        else:
            self.reply(404, b"Not found\n", [("Content-Type", "text/plain")])

    do_HEAD = do_GET

    def do_DELETE(self):
        self.reply(405, b"Method not allowed\n",
                   [("Content-Type", "text/plain"), ("Allow", "GET, HEAD")])

    def log_message(self, format, *args):
        pass


server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()

conn = http.client.HTTPConnection("127.0.0.1", server.server_address[1])
SHOW = ["Content-Type", "Content-Length", "Location", "WWW-Authenticate", "Allow"]

for method, path in [("GET", "/outages"), ("HEAD", "/outages"),
                     ("GET", "/outage-list"), ("GET", "/admin"),
                     ("DELETE", "/outages"), ("GET", "/tariffs"),
                     ("GET", "/report")]:
    conn.request(method, path)
    response = conn.getresponse()
    body = response.read()             # read it all before the next request
    print(f"{method} {path}")
    print(f"  HTTP/1.1 {response.status} {response.reason}")
    for name in SHOW:
        if response.getheader(name):
            print(f"  {name}: {response.getheader(name)}")
    if body:
        print(f"  body: {body!r}")

conn.close()
server.shutdown()
server.server_close()
GET /outages
  HTTP/1.1 200 OK
  Content-Type: application/json
  Content-Length: 15
  body: b'{"outages": []}'
HEAD /outages
  HTTP/1.1 200 OK
  Content-Type: application/json
  Content-Length: 15
GET /outage-list
  HTTP/1.1 301 Moved Permanently
  Content-Length: 0
  Location: /outages
GET /admin
  HTTP/1.1 401 Unauthorized
  Content-Type: text/plain
  Content-Length: 13
  WWW-Authenticate: Bearer realm="outage-api"
  body: b'Log in first\n'
DELETE /outages
  HTTP/1.1 405 Method Not Allowed
  Content-Type: text/plain
  Content-Length: 19
  Allow: GET, HEAD
  body: b'Method not allowed\n'
GET /tariffs
  HTTP/1.1 404 Not Found
  Content-Type: text/plain
  Content-Length: 10
  body: b'Not found\n'
GET /report
  HTTP/1.1 500 Internal Server Error
  Content-Type: text/plain
  Content-Length: 15
  body: b'Internal error\n'

Things to notice:

  • The HEAD response carries the same headers as GET, including Content-Length: 15, but no body.
  • The 301 has only a Location. A browser (or urllib.request) would now send a second request to /outages; http.client leaves that to you.
  • The 401 includes WWW-Authenticate to say which credentials are expected; the 405 includes Allow.
  • The server sets protocol_version = "HTTP/1.1" and always sends Content-Length, so the connection can be reused for all seven requests. Python's http.server is a teaching tool; its documentation says it is not recommended for production.

HTTP/1.1, HTTP/2 and HTTP/3

All three versions carry the same methods, status codes and headers. What changed is how messages travel, and every change targets the same problem: waiting.

Protocol stacks of the three HTTP versions. HTTP/1.1 runs as text over TLS over TCP over IP and handles one request at a time per connection. HTTP/2 runs as binary frames over TLS over TCP over IP and multiplexes many streams on one TCP connection. HTTP/3 runs over QUIC, which includes TLS 1.3, over UDP over IP; its streams are independent, so one lost packet does not stall the others. HTTP/1.1 HTTP/2 HTTP/3 HTTP/1.1 (text) TLS TCP IP HTTP/2 (binary frames) TLS TCP IP HTTP/3 QUIC (includes TLS 1.3) UDP IP one request at a time per connection many streams on one TCP connection independent streams, no TCP head-of-line wait
The semantics (top box) stay the same; the transport underneath changes. HTTP/3 moves encryption into the transport protocol.

HTTP/1.1

HTTP/1.1 (first published in 1997, now RFC 9112) sends readable text over a TCP connection. Connections are kept open for several requests, but on one connection the responses come back strictly in order: a slow response blocks everything behind it. Browsers work around that by opening up to about six connections per host, which costs extra handshakes. It is still everywhere: between proxies and applications, in embedded devices, and as the fallback when nothing else works.

HTTP/2

HTTP/2 (RFC 9113) keeps HTTP's semantics but sends messages as binary frames, and interleaves many requests and responses as separate streams over a single TCP connection (multiplexing). Headers are compressed (HPACK), which matters because the same cookies and user agent are repeated on every request. Browsers only use HTTP/2 over TLS.

HTTP/2 also introduced server push, letting the server send resources the browser had not asked for yet. It proved hard to use well; Chrome disabled it by default in version 106 (2022) and recommends 103 Early Hints instead: an interim response that tells the browser which resources to start fetching while the server is still preparing the page.

The remaining weakness lies below HTTP: TCP delivers bytes strictly in order, so one lost packet stalls all streams on the connection until it is retransmitted (TCP head-of-line blocking).

HTTP/3 and QUIC

QUIC (RFC 9000) is a transport protocol that runs over the User Datagram Protocol (UDP) and provides what TCP and TLS provided together: reliable, ordered, encrypted streams. Because it knows about the streams, a lost packet only delays the stream it belongs to. Encryption with TLS 1.3 is built in, so the connection and security setup take a single round trip, and a connection can survive a change of IP address, such as a phone moving from Wi-Fi to mobile data.

HTTP/3 (RFC 9114) is HTTP mapped onto QUIC. A browser usually first connects with HTTP/2 or HTTP/1.1, and the server advertises HTTP/3 in an Alt-Svc header (or in DNS); the next connection then tries QUIC on UDP port 443. As of October 2026, around 40% of websites support HTTP/3 according to W3Techs. The share of actual requests is lower: Cloudflare measured about a fifth of requests over HTTP/3 in 2025, because many clients and networks still fall back to HTTP/2.