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.jsonl to 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/models and 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-289 implements readUsageEntries() 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-126 calls summarizeUsage(readUsageEntries(), range, now, surface) for every GET /api/usage request.
gui/src/pages/Dashboard.tsx:322-329 includes /api/usage?range=30d in the same Promise.all as health, providers, and settings.
gui/src/pages/Dashboard.tsx:414-415 invokes 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.ts already contains readRecentUsageEntries(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 ms
Redacted configuration
Not configuration-specific.
Checks
Client or integration
OpenCodex dashboard
Area
Dashboard
Summary
The dashboard becomes progressively unresponsive as
usage.jsonlgrows because everyGET /api/usagerequest 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 backupGET /api/usage?range=30dThe 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 samePromise.allas health/providers/settings.After archiving the old log and starting a new
usage.jsonl,/api/usage?range=30ddropped to roughly 4–26 ms in repeated warm measurements (one later cold sample was 233 ms), and the other dashboard data appeared immediately.Reproduction
~/.opencodex/usage.jsonlto grow to tens of megabytes / hundreds of thousands of rows./healthz,/api/config,/api/providers, or/api/modelsand observe that otherwise-small management calls can be delayed.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: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.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:
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/opencodex2.7.40Operating 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
Redacted configuration
Not configuration-specific.
Checks