URLs and HTTP Basics
- A URL names a resource and tells the client how to reach it: the host goes to DNS, the port goes to TCP, the path and query go in the request line, and the fragment never leaves the browser.
- An HTTP message is text lines, then a blank line, then an optional body: a start line, header fields, an empty line, the content. A 70-byte
GETis a complete request. - Methods carry meaning.
GET,HEADandOPTIONSare safe;GET,HEAD,OPTIONS,PUTandDELETEare idempotent;POSTis neither, andPATCHis not guaranteed to be (RFC 9110, section 9.2). - HTTP is stateless: each request must carry everything the server needs, and the server may forget you the moment it replies. Logins survive because the client re-sends a cookie or token every time.
- Before the first byte of HTTP, a cold HTTPS fetch has already paid for DNS plus 3 round trips (TCP, TLS 1.3, then the request itself). Reusing the connection is what makes later requests cheap.
Mental model
Ordering at a counter with a paper form. The first line of the form says what you want (GET, the thing you want, the form version). The boxes underneath are headers: who you are, what you can accept, what language you prefer. A blank line ends the boxes, and anything stapled behind it is the body. The clerk reads the form, does the job, and hands back a form of their own: a result code, their own boxes, and the goods.
Where it breaks: a real counter clerk remembers your face. An HTTP server, by design, does not: the next form from you looks like a form from a stranger, so anything the clerk should remember (who you are, what is in your basket) has to be written on the form every time. A cookie is exactly that: a ticket the clerk gave you earlier, which you staple to every future form.
An HTTP exchange is a function call over the wire. The method is the verb, the URL identifies the resource being operated on, headers are metadata and context (like a call's options or an auth token), the body is the payload argument, and the status code is the return code.
Where it breaks: a function call shares memory and either returns or throws. An HTTP call shares nothing (every argument must be serialised into the message, which is why servers are "stateless"), and it can fail in the worst possible way: the request was applied but the reply was lost, so the caller sees only a timeout and cannot tell whether the work happened. That uncertainty is why idempotency matters so much later on this page.
How it works
Anatomy of a URL
A URL is five or six parts glued together by punctuation, defined by RFC 3986 section 3. Each part has a different consumer, and that is the useful way to remember them.
- Scheme (
http,https) picks the protocol and the default port: 80 forhttp, 443 forhttps. A URL may omit the port when it is the default, andHostomits it the same way. - Host is looked up by DNS to get an IP address. It is case-insensitive.
- Port is the TCP port the client connects to. See how a TCP connection opens.
- Path names the resource on that server. It is case-sensitive in general:
/Cartand/cartmay be different resources (RFC 3986 section 6.2.2.1 normalises only scheme and host). - Query (after
?) is extra parameters for the resource. Path and query together are the request target in the request line. - Fragment (after
#) names a part of the document the response returns, such as a heading. The client strips it before it builds the request, so the server never sees it (RFC 3986 section 3.5; the request target grammar in RFC 9112 section 3.2 has no fragment).
What an HTTP message looks like
HTTP/1.1 messages are plain text. A request is a request line, zero or more header fields, an empty line, and an optional body. A response has the same shape with a status line first (RFC 9112 section 2.1).
Here is that request exactly as it goes on the wire. Every line ends with CRLF (a carriage return then a line feed, two bytes, written \r\n in most languages); the last line shown below is the empty line that ends the headers.
The byte count, derived line by line (text length plus the 2-byte CRLF):
| Line | Text bytes | With CRLF |
|---|---|---|
| GET /index.html HTTP/1.1 | 24 | 26 |
| Host: www.example.com | 21 | 23 |
| Accept: text/html | 17 | 19 |
| (empty line) | 0 | 2 |
| Total | 62 | 70 |
So the request is 70 bytes, and it has 2 header lines (Host and Accept) plus the request line. That fits in a single TCP segment with a huge margin (a typical Ethernet segment carries up to 1,460 bytes of payload). A minimal reply to it:
The status line and four headers are 117 bytes including the final empty line (15 + 35 + 13 + 23 + 19 = 105 text bytes, plus 12 bytes of CRLF for the six lines), followed by the 137-byte body. For a small page the headers are close to half the response, which is one reason HTTP/2 compresses them (see HTTP/1.1 and HTTP/2). Status codes, redirects and the rules for retrying are covered on the HTTP status and semantics page; here we only need to know that the three-digit code in the status line is the return value.
Pick a scenario and step through a cold HTTPS exchange: DNS, TCP, TLS, then the request and response, with the raw bytes in a curl -v style transcript. The scenarios cover the main status classes; tick Edit to change the method, headers, body or status yourself:
* Host www.example.com resolved to 203.0.113.10Every scenario is a valid pairing. Edit mode lets you build pairs a real server would not send, such as a 304 with a body or a 201 with no Location; the rule check lists each one as text, but it does not stop you.
Methods: the verb, and what each one promises
The method tells the server what to do with the target and, more importantly for infrastructure, what the client promises about side effects. RFC 9110 section 9 gives each method three properties that caches, proxies and retry logic rely on.
| Method | Safe | Idempotent | Request body | Cacheable | Typical use |
|---|---|---|---|---|---|
| GET | yes | yes | no defined meaning | yes | read a resource |
| HEAD | yes | yes | no defined meaning | yes | headers only: existence, size, freshness |
| OPTIONS | yes | yes | optional | no | ask what a resource allows; CORS preflight |
| POST | no | no | yes | only with explicit freshness info (rare) | create or process: the catch-all |
| PUT | no | yes | yes | no | replace the resource at a known URL |
| PATCH | no | not guaranteed | yes | only with explicit freshness info (rare) | partial update |
| DELETE | no | yes | no defined meaning | no | remove a resource |
- Safe means the client does not request or expect any state change on the server, so it can be called freely by crawlers, prefetchers and retries.
- Idempotent means that sending the identical request N times has the same intended effect on the server as sending it once. The response may differ: the second
DELETEof the same URL typically reports that the thing is already gone. - Cacheable means a cache is allowed to reuse the response. RFC 9110 defines caching for GET, HEAD and POST, though almost every cache implements only GET and HEAD.
PATCHis defined in RFC 5789.
Headers: metadata about the message
Header fields are Name: value lines. They split by direction, though many apply to both. The old HTTP/1.1 term "entity headers" (describing the body) is called representation metadata in RFC 9110; in practice people still say entity headers for Content-Type, Content-Length and Content-Encoding.
| Header | Direction | Purpose | Example |
|---|---|---|---|
| Host | request | which site on this server (required in HTTP/1.1) | Host: www.example.com |
| Accept | request | media types the client can handle, with weights | Accept: text/html,/;q=0.8 |
| Accept-Encoding | request | compressions the client can decode | Accept-Encoding: gzip, br |
| Accept-Language | request | preferred natural languages | Accept-Language: en-US,en;q=0.9 |
| User-Agent | request | client software name and version | User-Agent: curl/8.7.1 |
| Authorization | request | credentials for the target | Authorization: Bearer TOKEN |
| Cookie | request | state the server asked the client to keep | Cookie: sid=abc123 |
| Content-Type | both | media type of the body | Content-Type: application/json |
| Content-Length | both | size of the body in bytes | Content-Length: 137 |
| Transfer-Encoding | both | how the body is framed | Transfer-Encoding: chunked |
| Connection | both | options for this connection only | Connection: close |
| Cache-Control | both | caching rules (see HTTP caching) | Cache-Control: max-age=3600 |
| Content-Encoding | response | compression applied to the body | Content-Encoding: gzip |
| Date | response | when the response was generated | Date: Sat, 10 Oct 2026 09:30:00 GMT |
| Server | response | server software | Server: nginx |
| Location | response | where to go next (redirects, created resources) | Location: /login |
| Set-Cookie | response | ask the client to store a cookie | Set-Cookie: sid=abc123; HttpOnly |
Cache-Control and validators are owned by HTTP caching. Cookies, sessions and tokens are owned by Cookies, sessions and CORS.
What happens before the request
HTTP does not start on its own. Before the first byte of the request the client has to find the server and open a channel. For https://www.example.com/ on a cold start:
- DNS: turn
www.example.cominto an IP address (DNS). Free if cached; otherwise at least one round trip to your resolver, more if the resolver has to walk the hierarchy. - TCP: the three-way handshake costs 1 RTT before the client can send data (TCP connection).
- TLS: for
https, a TLS 1.3 handshake adds 1 RTT, and TLS 1.2 adds 2 (TLS and HTTPS). - HTTP: the request and its response take 1 RTT plus the server's think time.
That is DNS + 3 RTT to the first byte of a cold HTTPS page, the same budget What Happens When You Type a URL walks through with caches and a CDN. HTTP/3 over QUIC merges the transport and TLS steps and lowers it.
One connection per object, or reuse it
Early HTTP (version 0.9 and 1.0) opened a fresh TCP connection for every object and closed it after the response: non-persistent. HTTP/1.1 keeps the connection open by default and reuses it: persistent.
For N objects fetched sequentially the totals are 2N RTT non-persistent (a handshake and a request each) and N + 1 RTT persistent (one handshake, then one request each). With N = 10 and a 50 ms RTT that is 20 RTT = 1,000 ms against 11 RTT = 550 ms, before counting TLS (which makes non-persistent worse again, one handshake per object). How browsers recover parallelism, and why one slow response still holds up the connection, is the next page: HTTP/1.1 and HTTP/2.
Deep dive
Safe, idempotent, cacheable: why the distinction exists
These three words are a contract between the client and the infrastructure in between.
- Safe lets a crawler or a link prefetcher call a URL without asking permission. A
GET /delete?id=7that actually deletes breaks that contract, and the first prefetcher or search crawler to visit the link will fire it. - Idempotent lets a client or proxy retry automatically after a communication failure. RFC 9110 section 9.2.2 says a client can repeat an idempotent request if the connection closed before it read the response, and that a client SHOULD NOT automatically retry a non-idempotent method and a proxy MUST NOT. Idempotency is about the intended effect on the server, not about identical responses.
- Cacheable lets a shared cache answer without bothering the origin.
PUT is idempotent because it replaces the resource with the supplied content: sending it five times leaves the same state as once. POST is not, because "process this" may mean "append another order". PATCH carries a delta, and a delta like "append this item" is not idempotent while "set status to shipped" is, so the RFC makes no promise either way. DELETE is idempotent even though the second call usually reports "not found": the intended effect, the resource being gone, is the same.
Why Host is mandatory
An IP address names a machine, not a website. Early servers hosted one site per IP address, but one server can host many sites on one address (virtual hosting), and IPv4 addresses were scarce. The only place the name can travel is inside the request, so HTTP/1.1 requires it: a client MUST send a Host header in every request, and a server MUST answer 400 to any HTTP/1.1 request with no Host, or with more than one (RFC 9112 section 3.2). HTTP/2 and HTTP/3 carry the same information in the :authority pseudo-header.
Host is not the only place the name appears. For https, the TLS ClientHello also carries it as SNI so the server can pick the right certificate before any HTTP is decrypted (see TLS and HTTPS). SNI chooses the certificate; Host chooses the site. If they disagree the server may reject the request; HTTP defines 421 Misdirected Request for requests sent to a server that cannot answer for that name (RFC 9110 section 15.5.20).
Where the body ends: Content-Length or chunked
A persistent connection carries message after message, so the receiver needs to know exactly where each body stops. HTTP/1.1 offers two ways, and a response to HEAD, a 204 and a 304 have no body at all (RFC 9112 section 6.3).
Content-Length: N: the body is exactly N bytes. Simple, but the sender must know the size before it starts.Transfer-Encoding: chunked: the body is a series of chunks, each prefixed with its size in hex, ending with a zero-size chunk. The sender can start streaming before it knows the total (generated pages, streamed responses). Thecurl --http1.1run in Hands-on shows a real server doing this.
A sender must not use both. If a message arrives with both, Transfer-Encoding wins and the receiver should treat the message as suspect.
HEAD and OPTIONS
HEAD is GET without the body: the server returns the same status and headers it would for a GET, which is how you check that a resource exists, how large it is (Content-Length) and whether it changed, without paying for the transfer. OPTIONS asks what a target supports, answered in an Allow header; browsers also use it for the CORS preflight (covered on the cookies, sessions and CORS page).
Content negotiation
One URL can have several representations (a language, a compression, a media type). In proactive negotiation the client states its preferences in request headers and the server chooses (RFC 9110 section 12):
Acceptfor media type (application/json,text/html).Accept-Encodingfor compression (gzip,br).Accept-Languagefor language (en-US).
Each value may carry a quality weight q from 0 to 1 (default 1): Accept: text/html,application/json;q=0.9,*/*;q=0.8 means "HTML first, JSON almost as good, anything else if you must". The server answers with Content-Type, Content-Encoding and Content-Language to say what it chose, and a Vary header to tell caches which request headers affected the choice (see HTTP caching).
Statelessness
RFC 9110 section 3 describes HTTP as a stateless request/response protocol: the server does not have to keep anything about a client between requests. That is a feature: any server behind a load balancer can answer any request, and a crashed server loses nothing about the protocol itself. Applications still need memory (who is logged in, what is in the basket), so they put it back by having the client send an identifier or credential on every request, usually a cookie or an Authorization header. The details belong to the cookies, sessions and CORS page.
Request smuggling, briefly
When a front-end proxy and a back-end server disagree about where one request ends (for example about Content-Length versus Transfer-Encoding), an attacker can hide a second request inside the first, and it is processed as if it arrived on its own. RFC 9112 section 11.2 names the risk, and PortSwigger's HTTP Desync Attacks research showed it working against real deployments. The defence is strict parsing and refusing ambiguous messages; see network attacks for the wider attack picture.
Textbook vs real world
| The textbook says | Production does | Why |
|---|---|---|
| HTTP is the protocol of web pages | Most clients are apps, mobile SDKs, service-to-service calls and webhooks; a browser is one client among many | The same request/response semantics work for any client that can open a TCP (or QUIC) connection, so APIs reuse them |
| Methods map to CRUD: POST create, GET read, PUT update, DELETE delete | POST is used for everything that is not a clean read, including reads that need a body (GraphQL queries are commonly POSTed), and GET sometimes mutates by mistake | POST is the catch-all with no stricter promise; a GET that mutates gets triggered by crawlers, prefetchers and retries |
| URLs are case-insensitive | Scheme and host are; path and query are case-sensitive on most servers | RFC 3986 normalises only scheme and host; the path is whatever the server decides it means |
| Every request opens a connection | Connections are persistent and browsers keep several open per host | Handshakes cost round trips; reuse pays them once |
| A header is a small thing | Headers are repeated in full on every HTTP/1.1 request, which is the overhead HTTP/2 and HTTP/3 compress | HTTP/1.1 has no compression for header fields, so a cookie is re-sent byte for byte on each request |
In production
- Stripe makes
POSTsafe to retry with anIdempotency-Keyheader: for the first request with a given key it saves the status code and body (after execution begins), returns that saved response on retries, errors if the retry's parameters differ, and may remove keys after they are at least 24 hours old (Stripe API: idempotent requests). - AWS EC2 takes a different route to the same end: a
ClientTokenrequest parameter makes an API call idempotent, so a retriedRunInstancesdoes not launch a second set of instances (EC2 API: ensuring idempotency). It is a parameter, not a header, which is why this page does not claim AWS "uses Idempotency-Key". - Idempotency keys are heading toward a standard. An IETF HTTPAPI working-group Internet-Draft defines an
Idempotency-Keyheader field (draft-ietf-httpapi-idempotency-key-header). Check its datatracker page for current status rather than trusting this page. - Reverse proxies add the client's address to the request because the origin otherwise sees only the proxy: the standardised header is
Forwarded(RFC 7239) and the older de facto one isX-Forwarded-For. The origin should trust only the entries added by proxies it controls, since a client can send its own. - Protocol versions in the wild move over time. Cloudflare Radar publishes the live split of HTTP/1.x, HTTP/2 and HTTP/3 requests (Radar adoption and usage); read the current figure there rather than a number frozen in a page.
History and evolution
- 1989 Tim Berners-Lee proposes a linked information system at CERN. By 1991 the prototype speaks what is now called HTTP/0.9: a single line,
GETfollowed by a path, answered with the raw document and then the connection closes. No headers, no status codes, no other methods (W3C: HTTP as implemented in 1991). - 1996 HTTP/1.0, RFC 1945. The web needed more than HTML: it adds the version in the request line, a status line, header fields,
HEADandPOST, andContent-Type. Still one TCP connection per request, which becomes slow as pages gain images. - 1997 HTTP/1.1, RFC 2068, revised in 1999 as RFC 2616. Connections become persistent by default, the
Hostheader becomes mandatory so one IP address can serve many sites, and chunked transfer and richer caching arrive. - 2014 the 2616 text is rewritten as six documents, RFC 7230 to 7235, to clarify and tighten the 1999 text.
- 2022 the specification is restructured again as RFC 9110 (semantics: methods, headers, status codes), RFC 9111 (caching) and RFC 9112 (HTTP/1.1 syntax). Splitting meaning from syntax lets HTTP/2 (RFC 9113) and HTTP/3 reuse the same semantics over a different wire format.
- Now HTTP/1.1 remains everywhere, with HTTP/2 and HTTP/3 carrying a large share of web traffic; the live split is on Cloudflare Radar. See HTTP/1.1 and HTTP/2 and QUIC and HTTP/3.
- Next the semantics stay stable while the wire changes underneath, and new methods arrive at the semantic layer. A
QUERYmethod (a safe request that carries a body) has been an IETF HTTP working-group draft; check its status before relying on it.
Hands-on
Needs only curl; the commands are the same on macOS and Linux. The output below is abbreviated from a real run on macOS with curl 8.7.1 (LibreSSL and SecureTransport); the TLS lines in the banner differ by OS and curl build (OpenSSL on most Linux), but the lines that start with > and < are the HTTP messages and look the same everywhere.
Read a request and response with curl -v:
Lines starting with > are what curl sent and < is what came back; lines starting * are curl's own notes. By default curl offers HTTP/2 over TLS (ALPN), so the request line says HTTP/2 and header names come back in lowercase. Force the text protocol described on this page:
Compare it with the 70-byte request on this page: curl adds User-Agent and Accept: */*. And Transfer-Encoding: chunked with no Content-Length is the chunked framing from the Deep dive. The final blank > and < lines are the empty lines that end the headers.
Send only the headers with HEAD:
-I sends HEAD, so there is no body. The allow: header shows this server permits GET and HEAD on that URL.
Send a form POST:
-d already switches the method to POST, so adding -X POST is redundant. Content-Length: 3 is the size of a=1, the body. The service is a public echo server and its output can change.
Type the protocol by hand with nc:
Then type a request line, a Host header, and press Enter on an empty line (the blank line is what makes the server answer):
The reply is the status line, headers and body as plain text, or a redirect to https since many sites no longer serve port 80 directly. nc sends a bare LF when you press Enter, not CRLF; servers generally accept it (RFC 9112 section 2.2 lets a recipient treat a lone LF as a line end), but a strict one may not. If it hangs, you probably missed the blank line.
Gotchas
Interview questions
Show answer
GET retrieves a resource: it is safe (no state change expected), idempotent and cacheable, and a body has no defined meaning, so parameters go in the query string. POST submits data for the server to process: it is neither safe nor idempotent, it carries a body, and its responses are cacheable only with explicit freshness information. In practice that means GET can be cached, prefetched and retried freely, and POST cannot.
Follow-ups:
- Why not put secrets in a query string? URLs get written to server and proxy logs, browser history and bookmarks, and sent in the
Refererheader to other sites (RFC 9110 warns about disclosing sensitive information in URIs). TLS encrypts the query in transit, so the leak is at the endpoints, not on the wire. - Is POST therefore secure? No. A POST body is not encrypted by being in the body; TLS protects both. POST only avoids the logging and history exposure of a URL.
Show answer
A request is a start line (method, target, version), header fields, an empty line, and an optional body. A response is a status line (version, status code, reason phrase), header fields, an empty line, and an optional body. Headers carry metadata such as Host, Content-Type and Content-Length; the body carries the content.
Follow-ups:
- What is the blank line for? It marks the end of the headers, so the parser knows where the body starts. After it,
Content-Lengthor chunked framing says how many body bytes to read. - How big is a small request? The 70-byte
GETon this page, line by line: 26 + 23 + 19 + 2.
Show answer
Each request carries everything the server needs to handle it; the server is not required to remember anything between requests, so any server behind a load balancer can answer any request. Logins persist because the server sends an identifier with Set-Cookie and the client returns it in a Cookie header on every later request (or sends an Authorization token), and the server looks the user up from it.
Follow-ups:
- Where does the session state live? Either server-side (the cookie is an ID into a store) or inside a signed token the client carries; Cookies, sessions and CORS covers the tradeoffs.
- Is that a violation of statelessness? No: the state is carried in each request rather than kept in the protocol.
Show answer
PUT replaces the resource at a known URL with the body you send, and is idempotent. PATCH applies a partial change (a delta) and is not guaranteed to be idempotent: "set status to shipped" is, "append this item" is not. POST asks the server to process the body, often to create a new resource under a collection with a server-chosen URL, and is not idempotent.
Follow-ups:
- Why does idempotency matter for retries? If a connection drops before the response arrives, a client can automatically retry an idempotent request knowing the intended effect is unchanged; it should not do that for a non-idempotent one without extra protection (RFC 9110 section 9.2.2).
- Does PUT creating a resource and PUT updating it break idempotency? No: the second call changes nothing; only the status code may differ.
Show answer
Host carries the site name and optional port from the URL, so a server with one IP address can host many sites (virtual hosting). The IP and port tell TCP which machine to connect to, but only the request can say which site, so HTTP/1.1 requires Host and a server must reject an HTTP/1.1 request without it with 400.
Follow-ups:
- How does it relate to SNI? The TLS ClientHello carries the name too, so the server can choose a certificate before any HTTP is read. SNI picks the certificate,
Hostpicks the site. HTTP/2 uses:authorityinstead ofHost. - What if they disagree? The server may reject the request; HTTP defines
421 Misdirected Requestfor a request sent to a server that cannot answer for that name.
Show answer
The scheme picks the protocol (and with it TLS and the default port), the host goes to DNS, the port goes to TCP, the path and query go into the request line, and the fragment is used only by the browser. So of https://www.example.com:8443/shop/cart?item=42#reviews the server sees GET /shop/cart?item=42 plus Host: www.example.com:8443.
Follow-ups:
- What happens to the fragment? It is stripped before the request is built, never sent, and resolved against the returned document in the browser.
- Which parts are case-sensitive? Scheme and host are not; path and query generally are.
Show answer
Have the client generate a unique idempotency key per logical operation and send it in a header (commonly Idempotency-Key) on every attempt of that operation. The server records the key with the outcome the first time, and when it sees the key again it returns the stored result instead of repeating the work.
Follow-ups:
- What does the server store? The key, enough of the request to detect misuse (Stripe errors if the same key arrives with different parameters), and the status and body of the first response, kept for a bounded time (Stripe's documentation says keys may be removed after at least 24 hours).
- What about two attempts racing in parallel? The server needs a lock or in-progress marker on the key so the second attempt waits or gets a conflict rather than running the operation twice.
Show answer
Non-persistent HTTP pays a TCP handshake per object, so fetching N objects in sequence costs 2N RTT (1 for the handshake, 1 for the request and response). Persistent connections pay one handshake and then one RTT per object: N + 1 RTT. With 10 objects at 50 ms that is 1,000 ms against 550 ms, before TLS. Browsers add parallelism by opening several connections per host.
Follow-ups:
- What is pipelining? Sending request 2 before response 1 has finished on the same connection. Responses must still come back in request order (RFC 9112 section 9.3.2), so one slow response blocks the rest, and buggy intermediaries made it unreliable, so browsers have disabled or removed it.
- How is multiplexing different? HTTP/2 splits messages into frames tagged with a stream ID and interleaves them on one connection, so responses can complete in any order. That removes HTTP-layer head-of-line blocking but not TCP's; see HTTP/1.1 and HTTP/2.
Fun facts
- The header is spelled
Referer(a misspelling of "referrer") in every HTTP specification. RFC 9110 keeps it and marks it with "[sic]" in its section 10.1.3. - HTTP/0.9 had exactly one method,
GET, a one-line request, no headers and no status codes. The server sent the document and closed the connection. - HTTP/1.1 messages end every line with CRLF, but RFC 9112 lets a recipient accept a lone LF. That leniency is part of why parsers disagree and request smuggling exists.
- The 70-byte request on this page is under 5 percent of the 1,460 bytes a typical Ethernet TCP segment can carry, so almost the whole segment is headroom.
Cheat sheet
- 70 bytes - the minimal
GET /index.htmlrequest on this page (24 + 21 + 17 text bytes plus 4 CRLFs, 8 bytes) - 117 bytes - the status line and four headers of the sample response, before its 137-byte body
- DNS + 3 RTT - cold HTTPS to first byte with TLS 1.3 (TCP, TLS, then the request); 0 DNS if cached, 1 RTT if the connection is reused
- 2N vs N + 1 RTT - N sequential objects, non-persistent vs persistent, ignoring TLS and transfer time
- 80 and 443 - default ports for
httpandhttps - Safe: GET, HEAD, OPTIONS. Idempotent: those plus PUT and DELETE. Neither: POST, and PATCH is not guaranteed.