Repository navigation
perf(server): usage summaries no longer wait on a forced pricing refresh (20 s → 2.7 ms median) - #30039
perf(server): usage summaries no longer wait on a forced pricing refresh (20 s → 2.7 ms median)#30039Yuruzuu wants to merge 1 commit into
Conversation
An ordinary usage read joined the pricing lock even while its cached rates were valid, so it waited behind a manual Refresh's HTTP attempts. Valid rates now skip the lock, each summary reports the pricing metadata of the table its buckets were priced with, and the in-flight scan key includes the table's fetch time so a manual Refresh's follow-up read never joins a scan priced before the new table was published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — Usage summaries can use still-valid cached pricing without waiting for a forced refresh, while keeping pricing metadata aligned with the summary. Existing freshness limits and refresh safeguards remain unchanged. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
|
Note Written by Verified in a real dev environmentverified as-is at ran one isolated dev server at a time on linux, using repository Playwright and warmed pricing from the real LiteLLM endpoint, then temporarily dropped pricing traffic at the network layer. after the refresh floor, client a clicked Refresh usage; client b changed from 30 days to 7 days. these are browser-observed RPC timings, one reproduction per revision, not a benchmark median: the pr rendered the new date range while the manual refresh remained pending. main kept the previous summary dimmed until the refresh finished. both returned the cached pricing timestamp, the scenarios use the same real transcript directory; totals grew between revisions because verification itself used real Codex turns. restored networking and checked a successful real refresh: 150 ms, followed by a 24 ms summary. both reported fresh pricing at
Not verifiednative desktop/mobile clients, relay/tunnel connections and other providers were not exercised. the exact mid-scan publication race and missing/24-hour-expired cache behavior passed deterministic tests but were not forced in the live app. Codex account limits could not be verified: the proxy returned JSON-RPC |



Problem
While one client runs a manual usage Refresh, every other ordinary usage summary read on that environment waits for the refresh to finish, even though its cached model rates are still valid. Against a slow or unreachable pricing endpoint, the refresh makes two sequential 10-second attempts (raw GitHub, then the GitHub API fallback), so those reads take about 20 s.
Change
In
apps/server/src/usage/UsageService.ts,ensureRatesacquiredratesLockbeforeloadRateschecked the 24-hour TTL. A non-forced read with a valid table therefore queued behind a forced refresh holding the lock for its HTTP attempts.<TTL boundary), missing caches and the 60 s refresh floor still go through the lock as before.scanSummarycaptures thepricingmetadata at the same moment it handsratesto theUsageAggregator. Before, it readpricing()at the end of the scan, so a refresh that landed mid-scan (for example during Codex service-tier resolution) labelled old-priced buckets with the new table'sfetchedAt. That race already exists onmain; the fast path would have made it easier to hit.scanKeynow includes the table's fetch time. The client's Refresh callsrefreshUsageRatesand then re-reads the summary. Without this, that re-read could join an in-flight scan priced with the old table. That case was possible onmaintoo and is more likely with the fast path.There are no contract, client or provider changes. Web, desktop and mobile share the same RPCs and gain the same benefit. I did not build stale-while-refresh pricing for cold or expired caches. That would need a pending-cost state in the clients.
Scope and approval
This is a focused performance and reliability fix, which CONTRIBUTING.md lists as most likely to be accepted, and it fits the small, focused bug-fix exception. Ordinary reads waited on an unrelated lock for no reason. The change is three small edits in one service. It adds no feature, changes no product defaults or contracts, and keeps manual Refresh semantics.
Verification
Benchmark: a temporary
it.livecase (not committed) uses the realUsageService, a 200-record Claude transcript, a seeded valid rates cache, and a localnode:httpstub that holds every request for 15 s, longer than the service's 10 s per-attempt timeout. Each repeat uses a fresh service. It startsrefreshRates, waits until the stub receives the request, then times one ordinaryreadSummary.mainReads with no concurrent refresh are unchanged within noise. I did not measure how often this convoy happens in real use. It needs a manual Refresh while the pricing endpoint is slow or unreachable, plus another read at the same time.
Tests:
vp test run src/usage/UsageService.test.ts -t "valid rates while a forced refresh|missing or has reached|pricing metadata of the rates|in-flight scan": 5/5 pass. All use TestClock and Deferred-gated fake HTTP, with no sleeps.serves an ordinary read from valid rates while a forced refresh is in flight: onmainit hangs until the timeout. On this branch the read returns old prices with old metadata, and the read after the refresh gets the new ones.waits for rates when the cache is missing or has reached its 24-hour expiry: passes on both. A missing cache waits for HTTP, 24 h − 1 ms uses the cache, and exactly 24 h fetches.reports the pricing metadata of the rates a summary was priced with: fails onmainwith mismatched metadata. It also checks that a read after a refresh doesn't join the older in-flight scan; without thescanKeychange that read hangs.vp run check:changed: format, lint, knip and typecheck pass. Two existing tests inUsageService.test.ts("discovers Pi history in its default home…" and "retains Codex reconciliation…") fail on macOS with the/private/varrealpath issue. They fail the same way on unmodifiedmain.I did not check this in a real client, against the real LiteLLM endpoint, or in a real offline outage.
An independent review (GPT-6.1 Terra via Codex) found the follow-up-read join issue, and it is fixed above. It found no mixed table/metadata publication, no TTL boundary drift, and no interruption or lock leak.
Made with Claude Opus 5.5 via Claude Code in T3 Code.
🤖 Generated with Claude Code