Repository navigation
Invariant 2 overstates what a cache can do: a revisit is free only inside the freshness window #49
Description
The claim
README.md, invariant 2:
A revisit costs zero remote round trips; conditional GET is answered inside localhost.
Measured
e2e/probe.mjs against real sites on a host over SSH, loading a page and then reloading it immediately:
| site | first load | revisit |
|---|---|---|
| Documenter output (QAtlas.jl docs) | 26 | 13 |
| Pinax board, 24 figures | 52 | 23 |
| Pinax board, 547 figures | 55 | 30 |
| small report, 9 requests | 18 | 0 |
Only the one that loads in under a second is free.
Cause
cache::DEFAULT_TTL is two seconds. Those pages take 2.5–3.6 s to load. The listings fetched at the start of a load have expired before the load finishes, so the reload re-fetches all of them.
Confirmed by changing only that constant. With the TTL at 120 s and nothing else touched:
| site | first load | revisit |
|---|---|---|
| Documenter output | 26 | 0 |
| Pinax board, 24 figures | 47 | 0 |
| small report | 21 | 0 |
The first-load cost barely moves, so this is entirely about the second visit. The invariant is exactly true with a longer TTL and false with this one.
The file bodies are not the problem — they are cached by (backend, path, mtime, size) and the conditional GET is answered locally as the README says. It is the listings that expire, and a listing is needed to resolve a path at all.
The trade
Two seconds was presumably chosen so that a file changed on the remote shows up almost at once — which is the right feel for "I just saved this, show me". A longer TTL is the right feel for "this is a built site, let me read it". Both are real, and the current value silently picks the first while the README promises the second.
Options, roughly in order of how much they cost to build:
- Raise the default. Simplest, and makes the documented invariant true. Costs the live-editing feel, and the number is still a guess.
- Make it configurable, with the default still to be chosen — so this does not answer the question, only moves it.
- Revalidate instead of expiring. Keep the listing indefinitely and check it against the remote only when a request would otherwise miss. A directory's own
mtimechanges when an entry is added or removed, so onestatper directory can confirm a whole listing — and that is a round trip, which is what the invariant is counting. Cheaper than re-listing, not free. - Tie freshness to the reader. A reload is a deliberate act and could be the thing that invalidates, rather than the clock.
My recommendation is (1) for now with a number chosen against the measurement rather than by feel, and (3) as the real answer — but the freshness/consistency trade is a product decision, not a bug with one right fix.
Whichever is chosen
The invariant has no test against a real transport. a_revisit_costs_no_remote_round_trips passes today because its fixture loads instantly, so the TTL never expires during it. A check that reloads after a delay longer than the TTL would have caught this from the start.
Activity
sotashimozono commentedon Sep 13, 2026
Correction: the cause I gave was wrong, and so were two of the numbers
I dug into this and most of what I wrote above does not survive. Recording it properly rather than quietly editing.
The daemon's cache is not the problem
Driven directly, with no browser, it does exactly what invariant 2 says:
Same through the alias Host and through the loopback fallback. Repeat reads cost nothing, ranges are sliced out of the held body, and the conditional GET is answered locally.
Two of my measurements were instrument error
page.reload()is not a revisit. A reload tells the browser to re-fetch. I should have navigated away and back. (Both turn out to give similar numbers here, but the first measurement was not of the thing being claimed.)"0 conditional requests" was false. I read
request.headers(), which is the pre-flight snapshot and does not include headers the browser adds later. Conditional requests are being sent — the responses include 304s, which cannot happen otherwise.Most of the request count was not ours. Of 412 browser requests on the Pinax board, 49 went to our origin. The other 342 are Chrome's own PDF viewer loading its internals (
pdf_embedder.css,mojo/.../bindings.js) once per embedded PDF. The board has 24 PDFs in it.What the revisit cost actually depends on
Not the page, and not the site. How long you wait before revisiting.
That is the freshness window behaving exactly as designed. Once a listing is not trusted, "is this still the current version?" cannot be answered without asking — so a revisit outside the window costs a listing refresh, by construction.
So the real problem is the claim, not the code
The second half is true and verified. The first half is true inside the freshness window and cannot be true outside it. As written it promises something no cache can deliver, and I should have read it that way before filing this as a bug.
What I tried, and am not proposing
I built the listing TTL as an idle window (fresh until 2 s after last use, capped at 30 s absolute), so that a page loading for four seconds cannot expire its own listings halfway through. It is a sound idea and it passed neutered tests.
It made no measurable difference. Load cost on the Pinax board was 48–50 round trips either way; on the Documenter page it moved from 26 to 34, in the wrong direction and inside the run-to-run spread. I have reverted it. A behaviour change to a public API on the strength of an argument that the measurement does not support is not worth shipping, and this repository's whole method is the other way round.
What is actually worth doing
a_revisit_costs_no_remote_round_tripspasses because its fixture loads instantly. A check that revisits inside the window, over SSH, would pin the part that is real.Retitling would be fair — this is "the README overstates invariant 2", not "a revisit is not free".
[-]A revisit is not free on a real site: the listing TTL is shorter than a page load[/-][+]Invariant 2 overstates what a cache can do: a revisit is free only inside the freshness window[/+]sotashimozono commentedon Sep 13, 2026
Closed by #51.
The README now says invariant 2 in the form that is true — zero inside the freshness window, one batched listing refresh outside it — and the e2e checks it over the real SFTP transport, where the claim is exact: a second read costs nothing, a range out of a held body costs nothing, and the browser's validator is answered locally with a 304. All three fail against a zero TTL; the unit tests pass unchanged under the same neutering, which is why both exist.
The bug I originally filed here did not exist. What did exist was an overstated claim and three measurements that disturbed what they measured. Both are fixed.