Repository navigation
Description
Client or integration
OpenCodex dashboard
Area
Dashboard
Summary
The dashboard becomes progressively unresponsive as usage.jsonl grows because every GET /api/usage request synchronously reads, splits, parses, normalizes, and summarizes the entire append-only file on the Bun server thread.
Observed on OpenCodex 2.7.40:
usage.jsonl: 81,556,089 bytes (77.8 MiB), 206,596 valid JSONL rows in the final backup- dashboard request:
GET /api/usage?range=30d - repeated pre-rotation latency: approximately 3.7–4.2 seconds per request
- dashboard polling interval: 5 seconds
- other small management endpoints: normally about 1–80 ms, but delayed while the synchronous usage read/parse occupied the server
The dashboard initially appeared offline or showed empty provider/settings state, then populated several seconds later. This was not caused by provider discovery: the slow endpoint was isolated to /api/usage, and the dashboard waits on it in the same Promise.all as health/providers/settings.
After archiving the old log and starting a new usage.jsonl, /api/usage?range=30d dropped to roughly 4–26 ms in repeated warm measurements (one later cold sample was 233 ms), and the other dashboard data appeared immediately.
Reproduction
- Run OpenCodex long enough for
~/.opencodex/usage.jsonlto grow to tens of megabytes / hundreds of thousands of rows. - Open the dashboard.
- Measure the usage endpoint repeatedly:
foreach ($i in 1..3) {
$sw = [Diagnostics.Stopwatch]::StartNew()
Invoke-RestMethod 'http://127.0.0.1:10100/api/usage?range=30d' | Out-Null
$sw.Stop()
"{0:N0} ms" -f $sw.Elapsed.TotalMilliseconds
}- While one usage request is running, request
/healthz,/api/config,/api/providers, or/api/modelsand observe that otherwise-small management calls can be delayed. - Archive/rotate
usage.jsonl, restart with a new empty live file, and repeat. In this case latency fell from about 4 seconds to tens of milliseconds.
The manual archive was only a temporary workaround. It reset the live dashboard's 30-day history even though the old rows were preserved in archive files.
Code-level evidence
In tag v2.7.40:
src/usage/log.ts:273-289implementsreadUsageEntries()as:
const lines = readFileSync(path, "utf-8").split(/\r?\n/);
for (const line of lines) {
const parsed = JSON.parse(line);
// normalize and append
}src/server/management/logs-usage-routes.ts:121-126callssummarizeUsage(readUsageEntries(), range, now, surface)for everyGET /api/usagerequest.gui/src/pages/Dashboard.tsx:322-329includes/api/usage?range=30din the samePromise.allas health, providers, and settings.gui/src/pages/Dashboard.tsx:414-415invokes that fetch immediately and then every 5 seconds.- The frontend has no in-flight guard, so polling can overlap once endpoint latency exceeds the interval.
src/usage/log.tsalready containsreadRecentUsageEntries(limit), which avoids loading the entire file for startup request-log hydration, but the dashboard usage route does not have an equivalent bounded/incremental path.
I did not find a user-configurable retention/rotation policy for usage.jsonl.
Expected behavior
The dashboard and management API should remain responsive as usage history grows. Please consider one or a combination of:
- Cache summaries and invalidate/update them only when the file grows.
- Maintain an incremental index or rolling aggregate instead of reparsing unchanged rows.
- Move expensive historical aggregation off the main server thread.
- Add bounded rotation/retention while preserving correct 7d/30d totals through aggregates or archived chunks.
- Prevent overlapping dashboard polls and decouple health/settings rendering from usage completion.
- Add a performance regression test with a large JSONL fixture (for example 75–100 MiB) that asserts unrelated management endpoints remain responsive.
The important contract is not a specific storage implementation: unchanged history should not be fully read and reparsed every five seconds, and a large usage log should not block unrelated management endpoints.
Version
@bitkyc08/opencodex 2.7.40
Operating system
Windows NT build 10.0.26200.8875 (x64)
Provider and model
Not provider-specific. The file contained usage from multiple routes/models.
Logs or error output
Before manual rotation:
usage.jsonl: 77.8 MiB, 206,596 rows
/api/usage?range=30d: approximately 3.7–4.2 s
After manual rotation:
/api/usage?range=30d: typically 4–26 ms
/api/config: approximately 7–16 ms
/api/providers: approximately 11–16 ms
/api/models: approximately 6–15 msRedacted configuration
Not configuration-specific.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Jul 26, 2026
Automated translation bookkeeping — detected language: English.
Ingwannu commentedon Jul 26, 2026
Confirmed against current
dev(911373db): this is a real performance defect./api/usagestill callsreadUsageEntries(), which synchronously reads and reparses the full JSONL, while the dashboard includes that request in its five-second core poll.I will address this in two layers:
I will add focused large-log/cache invalidation and dashboard polling regressions, then link the
devPR and measured results here before closing.Ingwannu commentedon Jul 26, 2026
Fixed and merged into
devvia PR #501.The landed design keeps the management path responsive without retaining an unbounded parsed-row cache:
usage.jsonlpaths are rejected explicitly, fixing the Windows behavior where a directory could otherwise look like an empty file.Release CI was also hardened from an 8-minute to a 12-minute ceiling for the full cross-platform job because the Windows suite was completing successfully but being cancelled during later privacy/build smokes.
Verification: 26 focused usage/API tests, 5 dashboard contract tests, full GUI tests (288), TypeScript typecheck, ESLint, production build, React Doctor, npm-global installation, and the complete Ubuntu/macOS/Windows CI matrix all passed. The final review queue had no unresolved threads.