---
title: 'HTTP Versions'
source: 'https://academia.sh/en/courses/application-protocols/http-versions'
course: 'Application Layer Protocols'
language: en
updated: '2026-08-17T18:07:00+00:00'
license: 'CC BY-SA 4.0'
---

# HTTP Versions

The same fields are measured across three framing regimes: the text line takes 3724 bytes and 251 blocking units, the binary frame that keeps context gives the same outcome on all 200 decisions for 440 bytes, and in the binary frame with no context 189 decisions move to the endpoints.

A cookie is a header field, and it is resent on every request within its scope. So is
the host name, so is the method, so are the preferences the client declares. In a
forty-exchange session, the same strings cross the wire forty times, written out from
scratch each time.

This repetition is the result of a design decision: an HTTP message is built from
**text lines**, and a text line is self-sufficient — the parser reads it, finds the
colon, separates name from value. The difference between versions is this lesson's
subject, and it is not in meaning but in **framing**: which fields travel does not
change, how they get packed onto the wire does. Our question — what does changing the
packing buy the intermediary in between, and what does it cost?

## Same Meaning, Different Framing

HTTP/1.1 writes a message line by line. The request line carries the method and path,
each following line is a header field, and a blank line announces that the body
starts. HTTP/2 and HTTP/3 put the same fields into **binary frames**: every frame has
a type, a length, a stream identifier, and flags; header fields are compressed with a
separate encoding.

```text
# teaching dump, not run

# text-line framing
GET /olcum/kuzey HTTP/1.1
Host: station.example
Cookie: oturum=k7
Accept: text/csv
Connection: keep-alive

# binary framing, same fields
FRAME type=HEADERS stream=7 flags=END_HEADERS
  :method     -> index
  :path       -> literal  /olcum/kuzey
  :authority  -> index
  cookie      -> index
  accept      -> index
FRAME type=DATA stream=7 flags=END_STREAM
```

The two forms say the same thing. Method, path, host, and the two preference fields
appear in both; `:method` and `:path` are the counterparts of the text-form request
line. Three things change: fields travel in **frames, not lines**, frames carry a
**stream identifier**, and header fields can be sent **by index**.

The stream identifier makes **multiplexing** possible: more than one exchange can
advance interleaved over a single connection, because every frame carries which
stream it belongs to. Text-line framing has no such field; requests on one connection
can only proceed one after another, and this holds for a **persistent connection**
too — the connection stays open, but order is preserved.

HTTP/3 changes not the framing but the **transport**. The frame layout stays similar;
the transport layer underneath offers independent streams instead of a single
ordered byte stream. Why this matters is the next section's subject.

## Head-of-Line Blocking in Two Layers

**Head-of-line blocking** is what happens when the item at the front of a queue
cannot advance, so everything behind it waits too. HTTP has two separate forms of
this, and confusing them makes the difference across versions invisible.

**The application-layer form:** if requests on one connection are sequential, a slow
response holds up every exchange behind it. The slowness does not come from the
network — the server is assembling that response from more than one record.
Multiplexing removes this form.

**The transport-layer form:** if the transport layer delivers in order, a lost
segment holds up everything that follows it, regardless of which stream it belongs
to, because the transport layer does not know about streams. Multiplexing does
**not** remove this form; it only moves it down from the layer above. What removes
it is the transport itself becoming stream-aware.

This distinction is a direct consequence of the byte-stream abstraction established
in the **Network Models and Protocols** course: once an ordered, lossless stream is
promised, keeping that promise requires a queue that waits. The introductory intuition
from **How the Internet Works** ties to a number here.

Blocking carries a separate meaning for the intermediary, which does not only read a
message but **forwards** it — that too is a queue. If application order is preserved,
the intermediary cannot send a response it holds before its turn comes, and it holds
up everything behind it while waiting for one response. What multiplexing removes is
not the server's queue but the intermediary's.

## Header Compression and the Intermediary's Context

Binary framing sends header fields by index: a name–value table is kept for the
connection's duration, and when a pair recurs, what is sent is not its name and
value but **its position in the table**. The gain is large, since fields like the
host name and the method almost never change.

The price: **an index is meaningless to whoever does not keep the table.** An
intermediary present since the connection began builds the same table and resolves
every field. One that joined later, or forwards frames without decoding them, can
read only the fields sent literally.

- **HA46** — The same forty exchanges, five fields, and oracle are used; only how
  the fields are written on the wire changes.
- **HA47** — In text-line framing, every field is written out from scratch in every
  message: name, value, separator.
- **HA48** — In binary framing, a name–value pair is written literally the first
  time it appears and enters the table; later occurrences send a **1-byte** index.
- **HA49** — An intermediary that keeps the table resolves every field. One that
  does not reads only fields sent literally; an indexed field does not exist for it.
- **HA50** — The exchange slow on the application side is `/ozet`; the server
  assembles that response from more than one record.
- **HA51** — The exchange that loses data in transport is chosen by a separate
  generator, with a single modulus.
- **HA52** — A blocking unit is a count, not a duration: an exchange waits one unit
  behind every blocker ahead of it in the same queue.
- **HA53** — In parallel connections, exchanges are distributed to connections in
  turn, and **each connection has its own table.**
- **HA54** — The measurement is not a network measurement; the byte count is a count
  of the message's content.

## The Measurement

```python
"""HTTP versions: what framing buys the intermediary, and what it costs.

Part 1 - the same fields across three framing regimes: decisions and bytes.
Part 2 - head-of-line blocking and multiplexing: blocked exchanges.
"""
SEED = 20260809
SAFE = {"GET", "HEAD"}
IDEMPOTENT = {"GET", "HEAD", "PUT", "DELETE"}
METHOD_POOL = ["GET", "GET", "GET", "HEAD", "POST", "PUT", "DELETE"]
PATH_POOL = ["/olcum/kuzey", "/olcum/yamac", "/ozet", "/kayit", "/oturum"]
DECISIONS = ("storable", "shareable", "fresh", "repeatable", "redirectable")
FIELDS = ("method", "path", "host", "private_marker", "validator")


def generator(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def exchanges(count=40):
    r, out = generator(SEED), []
    for i in range(count):
        method, path = METHOD_POOL[r(7)], PATH_POOL[r(5)]
        private = path == "/oturum" or r(5) == 0
        stale = r(3) == 0
        out.append({"num": i + 1, "method": method, "path": path,
                     "private": private, "stale": stale,
                     "private_marker": private and r(4) != 0,
                     "tag_missed": stale and r(6) == 0})
    return out


def oracle(a):
    return {"storable": a["method"] in SAFE and not a["private"],
            "shareable": not a["private"], "fresh": not a["stale"],
            "repeatable": a["method"] in IDEMPOTENT,
            "redirectable": True}


def message(a):
    return {"method": a["method"], "path": a["path"], "host": "station.example",
            "private_marker": a["private_marker"],
            "validator": a["stale"] and not a["tag_missed"]}


def infer(i):
    k = {}
    if "method" in i:
        k["repeatable"] = i["method"] in IDEMPOTENT
    if "private_marker" in i:
        k["shareable"] = not i["private_marker"]
        if "method" in i:
            k["storable"] = i["method"] in SAFE and not i["private_marker"]
    if "validator" in i:
        k["fresh"] = not i["validator"]
    if "host" in i:
        k["redirectable"] = True
    return k


def frame(batch, connections=1, compress=True):
    """Turns each message into a (bytes, literal-fields) pair.
    A name-value pair already seen on the same connection is sent as an
    index: it costs 1 byte, and an intermediary with no table cannot decode it."""
    table, out = [set() for _ in range(connections)], []
    for i, a in enumerate(batch):
        t, b, literal = table[i % connections], 0, set()
        for k, v in message(a).items():
            if not compress or (k, v) not in t:
                b += len(k) + len(str(v)) + 4
                literal.add(k)
                t.add((k, v))
            else:
                b += 1
        out.append((b, literal))
    return out


def measure(batch, frames, context):
    d = y = e = t = b = 0
    for a, (byte_cost, literal) in zip(batch, frames):
        visible = set(FIELDS) if context else literal
        truth = oracle(a)
        given = infer({k: v for k, v in message(a).items() if k in visible})
        missing = False
        for decision in DECISIONS:
            if decision not in given:
                e += 1
                missing = True
            elif given[decision] == truth[decision]:
                d += 1
            else:
                y += 1
        t += 1 if missing else 0
        b += byte_cost
    return d, y, e, t, b


def blocking_units(batch, blocked, connections=1):
    """An exchange waits one unit behind every blocker ahead of it in the
    same queue."""
    prior, total = [0] * connections, 0
    for i, blk in enumerate(blocked):
        total += prior[i % connections]
        prior[i % connections] += 1 if blk else 0
    return total


batch = exchanges()
loss_gen = generator(SEED + 1)
SLOW = [a["path"] == "/ozet" for a in batch]
LOST = [loss_gen(5) == 0 for _ in batch]
BOTH = [s or l for s, l in zip(SLOW, LOST)]
print(f"exchanges {len(batch)} | decisions {len(batch) * len(DECISIONS)} | "
      f"assembled response {sum(SLOW)} | lost in transport {sum(LOST)}")
print()
print("framing regime            correct  wrong  unavailable  extra round  bytes")
for name, frames, context in (
        ("text line", frame(batch, 1, False), True),
        ("binary, context kept", frame(batch), True),
        ("binary, no context", frame(batch), False)):
    d, y, e, t, b = measure(batch, frames, context)
    print(f"  {name:23s} {d:5d} {y:7d} {e:11d} {t:13d} {b:6d}")
print()
print("framing regime            app order  transport order  blocked exchanges")
for name, app_order, transport_order in (("text line, single connection", True, True),
                                          ("binary, single transport stream", False, True),
                                          ("binary, separate transport streams", False, False)):
    blocked = BOTH if app_order and transport_order else LOST if transport_order else [False] * len(batch)
    print(f"  {name:33s} {'yes' if app_order else 'no':>9s}"
          f" {'yes' if transport_order else 'no':>16s} {blocking_units(batch, blocked):18d}")
print()
print("parallel connections  blocked exchanges  literal fields   bytes")
for n in (1, 2, 6):
    c = frame(batch, n)
    print(f"  {n:19d} {blocking_units(batch, BOTH, n):19d}"
          f" {sum(len(s) for _, s in c):15d} {sum(b for b, _ in c):6d}")
```

```
exchanges 40 | decisions 200 | assembled response 9 | lost in transport 7

framing regime            correct  wrong  unavailable  extra round  bytes
  text line                 195       5           0             0   3724
  binary, context kept      195       5           0             0    440
  binary, no context         10       1         189            39    440

framing regime            app order  transport order  blocked exchanges
  text line, single connection            yes              yes                251
  binary, single transport stream          no              yes                 83
  binary, separate transport streams        no               no                  0

parallel connections  blocked exchanges  literal fields   bytes
                    1                 251              15    440
                    2                 121              29    661
                    6                  36              74   1397
```

## The Gain: Same Decision, an Eighth of the Bytes

The first table's first two rows should be read side by side. Text-line framing
gives **195** correct, **5** wrong, **0** unavailable, and spends **3724** bytes.
Binary framing that keeps context gives the **same** three numbers and spends **440**
bytes. The decision table does not change; bytes drop **88.2%**.

This is the shared setup's second claim read backwards. Adding a field did not buy a
decision; now it turns out that **making a field cheaper does not cost one either.**
As long as the intermediary can resolve a field, its byte cost is unrelated to the
decision. Two hundred field occurrences pass through the forty exchanges, and only
**15** are a distinct name–value pair; the remaining 185 repeat the same fifteen
pairs. This is exactly where compression gains ground.

The second table gives the second gain. Text-line framing on a single connection
produces **251** blocking units in total: nine assembled responses and seven losses
each hold up every exchange behind them. Once multiplexing removes application
order, blocking drops to **83** — all of it now from the transport layer. Once the
transport separates streams too, it is **0**.

The order of the three rows matters: multiplexing does not zero out blocking, it
**drops it to a third**; what zeros it out is the transport changing. The rest of the
problem was sitting one layer below the one framing solved.

## The Loss: An Unresolvable Field Is Ignored

The third row is this lesson's real finding. Bytes stay the same **440**, but the
decision table collapses: **10** correct, **1** wrong, **189** unavailable, **39**
extra round trips.

The reason: across forty messages, only **15** fields are written literally; the
remaining 185 are sent by index. An intermediary with no table cannot read those 185
fields, and it cannot draw a decision from a field it cannot read. **189** of the two
hundred decisions move from the middle to the endpoints — more even than the 160 the
envelope regime carried. The fields **sit there** in the message; they do not exist
for the intermediary.

The remaining eleven decisions come from the first exchange and the first few
occurrences where the table gets built. The single wrong decision sits there too:
the first exchange is `POST /kayit` and is personalized, but the server did not set
the flag. The intermediary can read the field in that message, so it decides, and
decides **wrong**. The pattern does not change — a readable field can be wrong; an
unreadable one is only missing.

The rule that follows: **compression is a visibility decision.** An intermediary
present from the connection's start loses nothing; one that joins later, or forwards
frames without decoding them, loses almost everything. Text-line framing had no such
split, because every message was self-sufficient.

## Parallel Connections: Another Price for the Same Problem

The third table measures the pre-multiplexing fix: opening more than one connection
to the same host. Blocking genuinely drops — from **251** to **121** on two
connections, to **36** on six.

The price sits in the two columns to the right. Every connection has its own header
table; as exchanges split across connections, the same name–value pairs get written
literally again on each one. The count of literal fields rises from **15** to **29**,
then to **74**; bytes rise from **440** to **661**, then to **1397**. Blocking
dropping to a seventh on six connections is paid for by bytes tripling.

This is where multiplexing's difference shows: on one connection, it **both** pushes
blocking down to the transport layer **and** keeps the table in one piece. Parallel
connections trade one cost for the other instead of removing either.

## Summary

- The difference between versions is not in meaning but in framing: the same fields
  are written as a text line or as a binary frame with a stream identifier.
- Binary framing that keeps context gives the **same** 195 correct and 5 wrong
  decisions for **440** bytes instead of **3724**; only **15** of the 200 field
  occurrences across forty exchanges are a distinct name–value pair.
- Head-of-line blocking has two forms; multiplexing removes the application-layer
  one and drops blocking from **251** to **83**, and only the transport gaining
  stream awareness drops the transport-layer form to **0**.
- An intermediary with no context reads only fields written literally: **189**
  decisions move to the endpoints, a debt of **39** round trips is created, and
  bytes do not change at all.
- Parallel connections drop blocking to **36** but split the table; literal fields
  rise to **74**, bytes to **1397**.

## Next Step

Framing determines whether the intermediary can read a field. But even when it can
read one, there is a decision that comes not from the field itself but from
**comparing two values**: does the copy on hand say the same thing as the message
that just arrived? The next lesson measures that comparison. Which field tells the
intermediary whether its stored copy is still valid, what that field's resolution
is, and what the intermediary says when the resolution falls short.
