Loading... [13 kB]
While testing my gopher client, I noticed something interesting: All downloads froze at 13 kilobytes. Sometimes this was barely noticeable, other times it would stall for a good second or so.
Networks operate on small chunks of data called packets. This allows resources to be shared between computers:
Instead of waiting for someone else's download to finish before loading a website, your web traffic can be sent in between the other user's packets. There's still some waiting involved, but it's on the order of milliseconds instead of hours.
... but it does create a problem if you're the one doing a big download. Because each packet is routed independently, they frequently arrive out-of-order or get lost in transit: Any large file would be hopelessly corrupted.
Transmission Control Protocol:
The obvious way to solve the first problem is to number each packet. Even if they arrive in the wrong order, the file can be put back together correctly.
For example, the string "Hello, World!" could be sent like this:#After a trip on the internet, it gets shuffled around:1 : "Hello " #2 : ", Wor " #3 : "ld! "
#... but can still be reassembled:2 : ", Wor " #3 : "ld! " #1 : "Hello "
"Hello , Wor ld! "
To deal with packet loss, the receiver responds to each data packet with an acknowledgment containing the next sequence number the computer expects.
Since communication is bidirectional, I'll call the computer sending data the "server", and the computer receiving it the "client":
Server sends: #1 : "Hello " #2 : ", Wor " #3 : "ld! "
... but packet #2 never arrives:
Client receives and responds: #1 : "Hello " -> ACK #2 (next please.)[ nothing ] #3 : "ld! " -> ACK #2 (got #3 , but I still need #2 !) Got: "Hello .....ld! "
... and few milliseconds later:
Server receives: ACK #2 ACK #2
After waiting for enough time to rule out packet reordering or jitter, the server assumes that packet #2 got lost and sends it again:
Server sends: #2 : ", Wor "
Client receives: #2 : ", Wor " -> ACK #4 (I already have #3 ) Got: "Hello , Wor ld! "
Now, the client has all the data, and the server knows it can safely continue or close the connection. Modern implementations also retransmit if they see enough duplicate acknowledgment numbers, which is faster then waiting for a timeout.
On paper, this is a great system, but in practice, it's very dangerous. If the network gets overloaded, routers are forced to drop packets. This results in systems constantly retransmitting data, creating more congestion, causing more packet loss...
To avoid breaking the internet (again, this happened in 1986), TCP adjusts it's "congestion window": The number of packets are sent ahead of the last acknowledgment. This is a roundabout way to cap the transfer speed, because it limits how much data is sent in a singe round trip.
At the start of the connection, TCP's "slow start" sets the window to 10 packets. Each (sequential) acknowledgment received increases the window by one, causing it to double each round trip.
Once packet loss is observed, the window is halved, and the algorithm switches to incrementing the transmit window by one packet each round trip. This "congestion avoidance" aims to track the maximum capacity of a network and evenly share bandwidth between systems.
There's a bit of subtly here with how control is handed around: If a packet loss is detected by repeated acknowledgment numbers, congestion control proceeds as described. If it's detected by timeout, the window is reset to 1 and "slow start" takes over until half the original window size.
The reasoning is that if the server gets a bunch of acknowledgments, this means that most of the packets have arrived and are no longer taking up network resources. If nothing is sent back, it's possible that those packets are still on the network and it should be careful — both to avoid creating congestion and to avoid wasting bandwidth on unnecessary retransmissions.
Anyways, if the network isn't congested, transfer speeds will ramp up exponentially, but that can take a while. With long ping times, the client might have to wait multiple seconds to get more than the 10 initial packets.
In my testing, most packets contained 1368 bytes of TCP payload, for a total of 13.2 kB... but some variation exists because of differences hardware along the path: different types of link can handle different amounts of data, and things like vlans can cut into the size limit.
Why should I care?
The practical takeaway is that size matters, even on gigabit connections.
During the first RTT, a client will receive somewhere on the order of 13 kB. After two, it'll receive twice that, for a total of ~40 kB. After three, it'll have got ~92 kB of data.
If you are writing a website, that first 13 kB should include everything needed to render the first screen's worth of content. This includes inlining any critical CSS or/and JavaScript.
... although, these aren't hard numbers, even though "Why your website should be under X kB" makes a good blog title. Setting aside network variations, HTTP headers and encryption can take up quite a bit of data. On the flip side, HTTP compression can save space.
Related:
- https://datatracker.ietf.org/doc/html/rfc9293: TCP standard.
- https://datatracker.ietf.org/doc/html/rfc5681: Congestion Control.
- /projects/gopher/: That gopher client