From a88f04dea259ad9e309fed6148f9732a536f77f0 Mon Sep 17 00:00:00 2001 From: Peter Hedenskog Date: Tue, 13 Oct 2026 08:31:39 +0200 Subject: [PATCH] Use a real network MTU when throttling localhost Loopback uses 65536-byte packets, so the server can send a whole page in one go and the browser's receive window becomes the limit. How fast that window opens depends on when the browser reads the data, so WebPageReplay runs fell into groups that were a whole round trip apart (FCP spread 300 ms for the same recording). With 1500-byte packets, the transfer grows like on a real network and the runs are steady (FCP spread 36 ms on the same page). Stop restores the loopback default. Assisted-by: Claude Opus 5.5 noreply@anthropic.com --- README.md | 2 ++ lib/localHostTc.js | 8 ++++++++ test/start.sh | 8 +++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3109922..b4c10c1 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ This is useful if you test a local web server or run [WebPageReplay](https://git throttle --rtt 200 --localhost ``` +On Linux, throttle also sets the MTU of the loopback interface to 1500, the packet size of real networks. With the loopback default (65536), the receive window of the browser limits the transfer, and the number of round trips changes from run to run. Stop sets the MTU back to 65536. If you do this on a host and not in a container, all local traffic uses the smaller packets until you stop. + ## Stop adding delay on localhost ``` diff --git a/lib/localHostTc.js b/lib/localHostTc.js index 19d4d51..d7f994a 100644 --- a/lib/localHostTc.js +++ b/lib/localHostTc.js @@ -1,5 +1,11 @@ import sudo from './sudo.js'; +// Real networks use 1500-byte packets. With the loopback default (65536), +// the receive window of the browser limits the transfer, and the number of +// round trips changes from run to run. +const NETWORK_MTU = '1500'; +const LOOPBACK_DEFAULT_MTU = '65536'; + export async function start(delay) { const halfWayDelay = delay / 2; @@ -22,7 +28,9 @@ export async function start(delay) { 'delay', `${halfWayDelay}ms` ); + await sudo('ip', 'link', 'set', 'dev', 'lo', 'mtu', NETWORK_MTU); } export async function stop() { + await sudo('ip', 'link', 'set', 'dev', 'lo', 'mtu', LOOPBACK_DEFAULT_MTU); await sudo('tc', 'qdisc', 'del', 'dev', 'lo', 'root'); } diff --git a/test/start.sh b/test/start.sh index a8c159e..968f66c 100755 --- a/test/start.sh +++ b/test/start.sh @@ -26,4 +26,10 @@ tc qdisc del dev "$INTERFACE" clsact ## Same thing when a plain ingress qdisc already exists tc qdisc add dev "$INTERFACE" ingress bin/index.js --profile cable -bin/index.js --stop \ No newline at end of file +bin/index.js --stop + +## Localhost sets a real network MTU and stop restores the loopback default +bin/index.js --rtt 200 --localhost +ip link show lo | grep -q 'mtu 1500' +bin/index.js --stop --localhost +ip link show lo | grep -q 'mtu 65536'