← Networking Mastery — Fundamentals to Principal

HTTP/1.0 & HTTP/1.1

HTTP/1.0 & HTTP/1.1

HTTP is the language the web speaks. Understanding it at this level — not just “it’s a request-response protocol” but why certain design decisions were made and what pain they cause — separates engineers who debug by guessing from those who debug by understanding.


HTTP Basics

Request-Response Model

Client sends a request. Server sends a response. That’s it. Every HTTP interaction is a single question and answer:

Client → "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"
Server → "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html>..."

The critical thing: HTTP is stateless. Each request is completely independent. The server has zero memory of previous requests from the same client. Cookies, sessions, JWTs — these are all application-level workarounds layered on top of stateless HTTP.

ELI5: HTTP is like ordering at a food counter where the cashier has amnesia. Every time you walk up, they have no idea who you are or what you ordered last time. Your loyalty card (cookie) is how you prove you’re a returning customer — the system, not the cashier, remembers you.

Why Text-Based?

HTTP headers are plain ASCII text, one header per line, terminated by \r\n. The entire header block ends with a blank line (\r\n\r\n). This makes HTTP:

  • Easy to debug with simple tools (curl -v, telnet, nc)
  • Readable by humans without special decoders
  • Parseable line-by-line, which was computationally cheap in the 1990s

The downside: verbose. A modern HTTP/1.1 request with cookies can easily hit 1-2KB of headers before a single byte of actual payload. This is one of the key problems HTTP/2 fixed with binary framing and HPACK compression.

HTTP Methods & When to Use Each

MethodIdempotentSafeUse For
GETYesYesFetch a resource, no side effects
HEADYesYesLike GET but headers only — check if resource exists/changed
OPTIONSYesYesDiscover allowed methods (CORS preflight uses this)
PUTYesNoReplace a resource entirely (idempotent upsert)
DELETEYesNoRemove a resource
POSTNoNoCreate new resource, trigger an action, submit a form
PATCHNo*NoPartial update (idempotent only if designed that way)

Idempotency means: calling it N times has the same effect as calling it once. PUT /users/42 with the same body 10 times = same result as 1 time. POST /orders 10 times = 10 orders created.

Why it matters for retries: When a network call fails and you don’t know if the server received it, you can safely retry idempotent requests. Retrying a non-idempotent POST can create duplicates. This is why payment systems use idempotency keys — they turn POST /charge into effectively idempotent.

ELI5: Idempotent is like a light switch vs a doorbell. Pressing the doorbell 5 times rings 5 times (not idempotent). Flipping a light switch to “on” 5 times still just leaves it on (idempotent). Safe means the action has zero side effects — like peeking at something without touching it.


HTTP/1.0 — The Beginning

HTTP/1.0 (1996) was designed for a web with simple pages: one HTML file, maybe a few images. The constraints were tight:

One request per TCP connection. After the server sends a response, the connection closes. Next request? New TCP handshake. This means every resource fetch (HTML + 3 images) costs 4 TCP handshakes plus 4 round trips minimum.

No Host header. HTTP/1.0 requests don’t include which hostname they’re targeting. The server just answers. This means one IP address = one website. Virtual hosting (multiple domains on one IP) was impossible. In 1996 with IPv4 addresses plentiful and sites few, fine. Today, catastrophic.

# HTTP/1.0 raw request — no Host header
telnet example.com 80
GET /index.html HTTP/1.0\r\n
\r\n
# Connection closes after response

Connection: keep-alive as a hack. Some servers and clients started supporting a non-standard Connection: keep-alive header to reuse connections. It worked, but was fragile — implementations varied, and both sides had to agree.

ELI5: HTTP/1.0 is like making a phone call to ask one question, hanging up, then calling again for the next question. Works fine if you have 3 questions. A modern website has 100+ questions (resources). That’s 100 phone calls just to load one page.


HTTP/1.1 — The Workhorse

HTTP/1.1 (1997, updated in 2014 with RFC 7230-7235) fixed the immediate pain points and ran the web for 15+ years.

Persistent Connections by Default

Connections stay open after a response. The server won’t close unless:

  • The response includes Connection: close
  • The idle timeout expires (typically 75s for Apache, 65s for nginx defaults)
  • The client closes it

This cuts TCP handshake overhead significantly. Loading a page with 30 resources goes from 30 handshakes to 1 (or a few, due to the browser’s connection-per-domain limit).

The Host Header — Virtual Hosting

HTTP/1.1 requires the Host header. This single change enabled the modern web:

GET /page HTTP/1.1
Host: blog.example.com

The same IP can now serve blog.example.com, shop.example.com, and api.example.com based purely on what Host the client sends. One IP, unlimited domains. This is how shared hosting works, how nginx virtual hosts work, how cloud load balancers route traffic.

ELI5: Before Host, an IP address was like a single-tenant apartment building — one IP, one website. The Host header turned it into a hotel — same building (IP), but you specify the room number (domain) and get routed to the right guest.

Pipelining — Good Idea, Terrible Reality

HTTP/1.1 specified that clients could send multiple requests without waiting for responses:

→ GET /style.css
→ GET /script.js
→ GET /logo.png
← 200 style.css content
← 200 script.js content
← 200 logo.png content

Sounds great. In practice, it was nearly unusable:

  1. Head-of-line blocking: The server must respond in the order requests arrived. If /style.css is slow (DB query, whatever), /script.js and /logo.png wait behind it — even if they’d be instant.
  2. Broken proxies: Many HTTP/1.1 proxies from the era didn’t implement pipelining correctly and corrupted responses.
  3. Retry complexity: If the connection drops mid-pipeline, the client can’t know which requests were processed.

Result: every major browser disabled pipelining. HTTP/1.1 effectively ran as “one request at a time per connection” in practice, despite the spec.

ELI5: Pipelining is like a fast food kitchen where you can shout all your orders at once. Sounds efficient. Except the cook must give you your burger before the next person’s salad, even if the salad is done first. So if someone ordered a slow dish first, everyone waits. The line backs up. It’s faster to just wait for each order confirmation before shouting the next one.

Chunked Transfer Encoding

HTTP/1.1 added Transfer-Encoding: chunked — allowing the server to stream a response without knowing the total size upfront:

HTTP/1.1 200 OK
Transfer-Encoding: chunked

4\r\n
Wiki\r\n
5\r\n
pedia\r\n
0\r\n
\r\n

Each chunk is preceded by its hex size. A 0 chunk signals the end. This enables:

  • Streaming responses (server-sent events, long-running queries)
  • Proxies forwarding responses before the upstream finishes
  • Dynamic content where total size isn’t known at start

Content-Length vs Transfer-Encoding: chunked: you can only use one. If you know the size upfront, Content-Length is preferred (allows the client to show progress bars, pre-allocate buffers). If you’re streaming, use chunked.


Headers Deep Dive

Key Request Headers

HeaderPurposeExample
HostTarget virtual host (required in HTTP/1.1)Host: api.example.com
AcceptMedia types client understandsAccept: application/json, text/html
Accept-EncodingCompression algorithms supportedAccept-Encoding: gzip, br
AuthorizationCredentials (Bearer token, Basic auth)Authorization: Bearer eyJ...
CookieSession cookies set by serverCookie: session=abc123
User-AgentClient identificationUser-Agent: Mozilla/5.0...
RefererPage that linked to this requestReferer: https://google.com/
If-None-MatchETag for conditional requestsIf-None-Match: "abc123"
If-Modified-SinceTime-based conditional requestIf-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

Key Response Headers

HeaderPurposeExample
Content-TypeMIME type of response bodyContent-Type: application/json; charset=utf-8
Cache-ControlCaching directivesCache-Control: max-age=3600, public
ETagResource version identifierETag: "33a64df5"
Last-ModifiedLast change timestampLast-Modified: Tue, 15 Nov 2024 12:00:00 GMT
Set-CookieSet client cookieSet-Cookie: session=abc; HttpOnly; Secure
LocationRedirect targetLocation: https://example.com/new-path
VaryWhich request headers affect cachingVary: Accept-Encoding

Content Negotiation

Client and server agree on format via Accept/Content-Type:

# Client asks for JSON, server responds with JSON
curl -H "Accept: application/json" https://api.example.com/users

# Server confirms format in response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

The Vary header tells caches that the cached response only applies to requests with the same Accept-Encoding (or other listed headers). Without Vary: Accept-Encoding, a cache might serve a gzipped response to a client that can’t decompress it.

Hop-by-Hop vs End-to-End Headers

End-to-end headers travel all the way from client to server (or server to client) unchanged. Proxies forward them. Examples: Content-Type, Authorization, Cache-Control.

Hop-by-hop headers apply only to the immediate connection and must not be forwarded. Examples: Connection, Keep-Alive, Transfer-Encoding, Upgrade.

Why this matters: If you’re debugging a proxy stripping your Authorization header, check if the proxy is treating it as hop-by-hop (it shouldn’t be). Also, Connection: keep-alive is hop-by-hop — a proxy should not forward it to the upstream; it should manage its own connection separately.


Caching

Caching is the single biggest performance lever in HTTP. Done right, you serve millions of users from memory. Done wrong, users see stale data for days.

Cache-Control Directives

DirectiveApplies ToMeaning
max-age=NBothResponse is fresh for N seconds
s-maxage=NResponseShared cache (CDN) max age, overrides max-age
no-cacheBothMust revalidate before serving (not “don’t cache”)
no-storeBothNever cache, never store on disk
publicResponseAny cache can store (even shared CDN caches)
privateResponseOnly browser cache, not CDN (e.g., user-specific data)
must-revalidateResponseOnce stale, must revalidate before serving
stale-while-revalidate=NResponseServe stale for up to N seconds while fetching fresh
immutableResponseContent will never change; don’t revalidate even on reload

Common mistake: no-cache does NOT mean “don’t cache.” It means “cache it, but always check with the server before using it.” no-store is “don’t cache.” Getting this wrong means either stale data for users or unnecessary load on your origin.

Conditional Requests

ETags and Last-Modified let the browser ask: “has this changed since I last got it?”

# First request — server sends ETag
GET /api/users HTTP/1.1
200 OK, ETag: "v42", body: [...]

# Second request — client sends ETag back
GET /api/users HTTP/1.1
If-None-Match: "v42"
304 Not Modified (no body!)

The 304 response has no body — the browser uses its cached copy. This saves bandwidth without serving stale data.

ELI5: ETags are like a version number stamped on your library book. When you return it and ask for “the latest version,” the librarian checks: “You have v42, current is v42 — you’re good, no need to re-issue.” You walk out with nothing (304) but that’s the point — you already have the book.

Cache Layers

Browser Cache → CDN Edge → Reverse Proxy → Origin Server
     (private)   (public, shared)  (internal)    (source of truth)

Each layer can cache. A CDN hit saves a round trip to your data center. A reverse proxy hit (nginx, Varnish) saves hitting your app server. Browser cache is instantaneous.

Stale-while-revalidate is underused gold: serve the cached version immediately (fast), kick off a background revalidation request, update cache for next visitor. Users get instant responses; you always converge to fresh data.

Cache Invalidation

The classic: “There are only two hard things in Computer Science: cache invalidation and naming things.”

Strategies:

  • URL versioning: /app.v42.js — when content changes, change the URL. Old URL stays cached forever (set max-age=31536000, immutable). Deploy new URL.
  • Content-hash in URL: /app.a3f2c1.js — hash changes automatically when content changes.
  • CDN purge API: Explicitly invalidate URLs in your CDN on deploy. AWS CloudFront, Cloudflare both have APIs for this.
  • Surrogate keys / Cache tags: Tag cached responses with logical keys (e.g., user:42), purge all responses tagged with that key when data changes.

Common mistake: Setting Cache-Control: no-cache on your API responses because you’re worried about stale data. Better: use short max-age (30s-60s) + stale-while-revalidate. Users get fast responses; worst case, 30 seconds stale.


Connection Management

The 6-Connection-Per-Domain Browser Limit

Browsers limit concurrent connections to 6 per hostname. This is a TCP-level constraint to be a “good citizen” and not overwhelm servers. But it means if you have 30 resources on example.com, 6 load simultaneously and the rest queue.

Domain sharding was the HTTP/1.1 era fix: spread resources across static1.example.com, static2.example.com, static3.example.com. 6 connections per domain × 3 domains = 18 concurrent connections.

This is a hack. It adds DNS lookups, extra TLS handshakes, and complexity. HTTP/2 multiplexing eliminates the need entirely — one connection handles all resources concurrently.

Common mistake: Still doing domain sharding in 2025 when your CDN supports HTTP/2. You’re adding latency (extra DNS + TLS) instead of removing it. Domain sharding actively hurts on HTTP/2.

Keep-Alive Timeouts

Servers close idle persistent connections after a timeout. Common defaults:

  • nginx: 75 seconds
  • Apache: 5 seconds (very conservative)
  • AWS ALB: 60 seconds
  • Cloudflare: 400 seconds to origin

When a client reuses a connection that the server already closed, it gets a TCP RST and has to reconnect. This causes intermittent errors that are hard to reproduce. The fix: set client-side keep-alive below server-side timeout. If server closes at 60s, client should close (or refresh) at 55s.

Connection Pooling in Backend Services

Every internal service call (your API → database, your API → upstream API) should use connection pooling. Opening a new TCP + TLS connection for every request can add 50-200ms of overhead.

Without pooling: [DNS] → [TCP handshake] → [TLS] → [Request] → [Response] → [Close]
With pooling:    [Request] → [Response]  (connection already open and waiting)

Most HTTP clients (axios, requests, Go’s http.Client) pool by default. The gotcha: pool size. Too small → requests queue waiting for a connection. Too large → you overwhelm the downstream service with concurrent connections.


HTTP Status Codes

Quick Reference

RangeMeaningCommon Examples
1xxInformational100 Continue, 101 Switching Protocols
2xxSuccess200 OK, 201 Created, 204 No Content
3xxRedirect301, 302, 307, 308
4xxClient Error400, 401, 403, 404, 409, 422, 429
5xxServer Error500, 502, 503, 504

The Redirect Maze

CodePermanent?Method Preserved?Use When
301YesNo — POST → GETPermanent redirect, old URL → new URL
302NoNo — POST → GETTemporary redirect (like after login)
307NoYes — POST stays POSTTemporary redirect preserving method
308YesYes — POST stays POSTPermanent redirect preserving method

Common mistake: Using 301 for API redirects when the client is POSTing. The client’s POST /v1/users becomes a GET /v2/users and breaks silently. Use 307 or 308 for API version redirects.

The 4xx Decision Table

SituationStatus Code
Malformed request syntax400 Bad Request
Not authenticated (no/invalid credentials)401 Unauthorized
Authenticated but no permission403 Forbidden
Resource doesn’t exist404 Not Found
HTTP method not supported for this endpoint405 Method Not Allowed
State conflict (duplicate, version mismatch)409 Conflict
Validation error (valid syntax, invalid semantics)422 Unprocessable Entity
Rate limit exceeded429 Too Many Requests

Common mistake: Using 403 when the user isn’t logged in. 401 means “you need to authenticate first.” 403 means “I know who you are, you just can’t do this.” Different error, different client behavior.

5xx — Gateway Errors You’ll See Constantly

  • 502 Bad Gateway: Your proxy/load balancer got an invalid response from the upstream. Usually means the upstream crashed or returned garbage.
  • 503 Service Unavailable: Server is up but overloaded, or in maintenance. Should include Retry-After header.
  • 504 Gateway Timeout: Proxy timed out waiting for upstream. Upstream is alive but too slow.

ELI5: 502 is your manager getting garbled nonsense back from the supplier. 503 is the supplier saying “we’re slammed, call back later.” 504 is calling the supplier, waiting on hold for 30 minutes, and hanging up.


HTTP/1.1 Performance Problems

These aren’t theoretical — they’re why HTTP/2 was built.

Head-of-Line Blocking (Application Level)

Even with persistent connections, HTTP/1.1 is still strictly sequential per connection. One slow response blocks everything behind it on that connection. With 6 connections and 30 resources, you’ll get blocking cascades.

Connection 1: [slow resource 200ms] [queued] [queued]
Connection 2: [fast 10ms] [fast 10ms] [fast 10ms]
...

The 6 fast resources on connection 2 finish in 30ms. The 3 queued on connection 1 sit waiting 170ms for no reason.

Header Overhead

Every HTTP/1.1 request sends all headers from scratch. With cookies, this can easily be 1-2KB of overhead per request. A page with 100 resources = 100-200KB of redundant header data sent over the wire.

# Example: just the cookie header
Cookie: _ga=GA1.2.xxx; session=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyMzQ1...
# That's 200+ bytes. On every single request. For every resource.

HTTP/2 HPACK compression brings repeated headers down to a few bytes via a shared header table.

The Workarounds (And Why They’re Hacks)

HackProblem It SolvesWhy It’s Bad
Sprite sheetsReduce image requestsImages hard to maintain, no partial loading
JS/CSS concatenationReduce file requestsOne change invalidates entire bundle cache
InliningZero requests for small assetsAssets not cacheable separately
Domain shardingMore concurrent connectionsMore DNS, more TLS handshakes, complex invalidation

HTTP/2 eliminates all of these needs. HTTP/1.1 optimization and HTTP/2 optimization are sometimes opposite — what helps on HTTP/1.1 (domain sharding, concatenation) hurts on HTTP/2.

ELI5: These workarounds are like organizing a bucket brigade to fill a swimming pool because you only have a garden hose. It works, technically, but you’re solving a plumbing constraint problem with people-logistics solutions. HTTP/2 gives you a fire hose — you stop needing the brigade.


Summary

ConceptHTTP/1.0HTTP/1.1
Persistent connectionsNo (or hack)Yes (default)
Host headerNot supportedRequired
PipeliningNoYes (but broken in practice)
Chunked transferNoYes
Virtual hostingNoYes
Head-of-line blockingPer connectionPer connection (same)
Browser connections1 per domain6 per domain
Caching Quick ReferenceWhen to Use
Cache-Control: max-age=31536000, immutableStatic assets with hash/version in URL
Cache-Control: max-age=60, stale-while-revalidate=300Frequently-read API data
Cache-Control: no-storeSensitive user data (bank statements, medical records)
Cache-Control: private, max-age=300User-specific data (profile page)
ETag + If-None-MatchAny resource worth caching but may change

HTTP/1.1’s core limitation is one-response-at-a-time per connection with text-based verbose headers. Everything else — pipelining failures, domain sharding, concatenation — flows from that. HTTP/2 attacks both problems directly: binary multiplexed framing + HPACK header compression.