Perf: one page load sends the same API requests 2–4 times #140

Closed
opened 2026-09-25 22:18:02 +00:00 by kayg · 7 comments
Owner

Severity: low (performance first)

Problem

One cold load of a Files route (320, production build) sends the same API request several times:

Request Count per load
/api/v1/files/entries 2–4 (4 for a missing folder)
/api/v1/search/saved 2–3
/api/v1/auth/sessions 2
/api/v1/search 2–3

Expected

One request per resource per load (shared stores, de-duplicated in-flight requests).

Also seen once, not reproduced: /files/trash at 320 logged 348 × net::ERR_INSUFFICIENT_RESOURCES late in a long run (the browser ran out of connections). Watch for a leaked EventSource or retry loop.

Found by the break-it sweep (#117). Re-run: cd apps/web && bun run build && bun e2e/breakit.mjs --keep <dir> (script on branch job/breakit-fixes). Screenshots: /home/kayg/Developer/calternal/target/breakit (run 1 in run1/, fix checks in verify/).

**Severity:** low (performance first) ## Problem One cold load of a Files route (320, production build) sends the same API request several times: | Request | Count per load | |---|---| | `/api/v1/files/entries` | 2–4 (4 for a missing folder) | | `/api/v1/search/saved` | 2–3 | | `/api/v1/auth/sessions` | 2 | | `/api/v1/search` | 2–3 | ## Expected One request per resource per load (shared stores, de-duplicated in-flight requests). Also seen once, not reproduced: `/files/trash` at 320 logged 348 × `net::ERR_INSUFFICIENT_RESOURCES` late in a long run (the browser ran out of connections). Watch for a leaked EventSource or retry loop. Found by the break-it sweep (#117). Re-run: `cd apps/web && bun run build && bun e2e/breakit.mjs --keep <dir>` (script on branch `job/breakit-fixes`). Screenshots: `/home/kayg/Developer/calternal/target/breakit` (run 1 in `run1/`, fix checks in `verify/`).
Author
Owner

The production files E2E counted duplicate GETs on a cold Files load: /api/v1/files/entries?path=Inbox&limit=500&sort=name was requested twice, and /api/v1/search/saved was requested twice. The shared transport cleared each request when headers arrived, before body decoding finished. I have extended its pending window through response body transfer and am rerunning the regression.

The production files E2E counted duplicate GETs on a cold Files load: `/api/v1/files/entries?path=Inbox&limit=500&sort=name` was requested twice, and `/api/v1/search/saved` was requested twice. The shared transport cleared each request when headers arrived, before body decoding finished. I have extended its pending window through response body transfer and am rerunning the regression.
Author
Owner

During the #117 full UI matrix, three routes at 1024px physical width / 512 CSS px (200% zoom) logged auth rate-limit responses after hundreds of sequential page loads. /api/v1/auth/me and /api/v1/auth/sessions returned 429 on the month, year, and invalid-date calendar routes; later profiles stopped seeing the 429s. This appears to be the sweep’s repeated app-load request volume reaching the existing auth limit, not a 5xx or persistent failure.

Example screenshots:

  • /home/kayg/Developer/calternal/target/breakit/cont-breakit-117/full-final/cal-month_1024z200-paper.png
  • /home/kayg/Developer/calternal/target/breakit/cont-breakit-117/full-final/cal-year_1024z200-paper.png
During the #117 full UI matrix, three routes at 1024px physical width / 512 CSS px (200% zoom) logged auth rate-limit responses after hundreds of sequential page loads. `/api/v1/auth/me` and `/api/v1/auth/sessions` returned 429 on the month, year, and invalid-date calendar routes; later profiles stopped seeing the 429s. This appears to be the sweep’s repeated app-load request volume reaching the existing auth limit, not a 5xx or persistent failure. Example screenshots: - `/home/kayg/Developer/calternal/target/breakit/cont-breakit-117/full-final/cal-month_1024z200-paper.png` - `/home/kayg/Developer/calternal/target/breakit/cont-breakit-117/full-final/cal-year_1024z200-paper.png`
Author
Owner

Correction after retesting: the shared GET promise now remains pending through response-body transfer, and the duplicate Files E2E still reports two requests each for the Inbox listing, root listing, and saved searches. The earlier body-lifetime hypothesis was insufficient. I am tracing the call sites and will keep the issue open until the browser regression check passes.

Correction after retesting: the shared GET promise now remains pending through response-body transfer, and the duplicate Files E2E still reports two requests each for the Inbox listing, root listing, and saved searches. The earlier body-lifetime hypothesis was insufficient. I am tracing the call sites and will keep the issue open until the browser regression check passes.
Author
Owner

Further trace: the second saved-search GET comes from the sidebar watcher after SearchDialog has already loaded the same shared store. The store now shares an active load and keeps its resolved snapshot until a visibility refresh. I am verifying this in a rebuilt production E2E; the duplicate Files listing still needs a confirmed cause.

Further trace: the second saved-search GET comes from the sidebar watcher after SearchDialog has already loaded the same shared store. The store now shares an active load and keeps its resolved snapshot until a visibility refresh. I am verifying this in a rebuilt production E2E; the duplicate Files listing still needs a confirmed cause.
Author
Owner

Production trace after the saved-store fix: now fires once. The Files listing still fires twice; the first response finished 363 ms before the second request began. The second caller is a Files refresh from the live change stream, which replays recent events on a cold connection. I am extending the shared client coalescer to cover this short stagger between shell/page loads, with a bounded response window and write invalidation.

Production trace after the saved-store fix: now fires once. The Files listing still fires twice; the first response finished 363 ms before the second request began. The second caller is a Files refresh from the live change stream, which replays recent events on a cold connection. I am extending the shared client coalescer to cover this short stagger between shell/page loads, with a bounded response window and write invalidation.
Author
Owner

Production trace after the saved-store fix: /api/v1/search/saved now fires once. The Files listing still fires twice; the first response finished 363 ms before the second request began. The second caller is a Files refresh from the live change stream, which replays recent events on a cold connection. I am extending the shared client coalescer to cover this short stagger between shell/page loads, with a bounded response window and write invalidation.

Production trace after the saved-store fix: /api/v1/search/saved now fires once. The Files listing still fires twice; the first response finished 363 ms before the second request began. The second caller is a Files refresh from the live change stream, which replays recent events on a cold connection. I am extending the shared client coalescer to cover this short stagger between shell/page loads, with a bounded response window and write invalidation.
Author
Owner

Fixed. The API client tests passed for shared concurrent GETs, independent caller responses, caller aborts, staggered reads, and dropping cached snapshots after writes or expiry. The Files E2E also passed its duplicate-request check. The shared in-flight request mechanism is in packages/api-client and is used by the handwritten Notes clients.

Fixed. The API client tests passed for shared concurrent GETs, independent caller responses, caller aborts, staggered reads, and dropping cached snapshots after writes or expiry. The Files E2E also passed its duplicate-request check. The shared in-flight request mechanism is in `packages/api-client` and is used by the handwritten Notes clients.
kayg closed this issue 2026-09-26 12:52:16 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#140
No description provided.