Skip to content

[Bug]: OpenCode usage scan takes ~5 min on a large OpenCode 2 database, repeats on every refresh and stalls the server #28430

Closed as duplicate

Description

@nkoynov

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server (packages/provider-opencode/src/server/usage.ts, apps/server/src/usage/UsageService.ts)

Steps to reproduce

  1. Run a T3 server whose OpenCode 2 instance has a large opencode.db. Here: OpenCode 2.2.5, 9.9 GB database, 481,648 session_message rows, 436,585 of them type = 'assistant' inside a 30-day window (about 29k per day; average data length 14 KB).
  2. From a desktop client connected to that server, open Usage, choose "All environments" and 30 days.
  3. Close and reopen the page more than 5 minutes later, or press refresh.

Expected behavior

The remote environment's usage arrives in seconds, and repeat opens reuse earlier work. While an environment is still loading, the totals say they are partial.

Actual behavior

  • server.getUsageSummary took 278 s. Almost all of it was readOpenCodeUsage (276 s). It pages session_message by rowid from MIN(rowid) to MAX(rowid), 64 rows at a time, and runs JSON.parse on the full data of every in-window assistant row. The other readers finished in under 3 minutes, in parallel.
  • The OpenCode reader has no cache, so each new summary request repeats the full 4.6-minute scan.
  • During the scan, the server's event loop sat at 100% utilization, with stalls of 2.4 to 4.3 s and RSS rising to 1.5 GB. Live agent turns on that server share this event loop.
  • The page draws "All environments" totals before the slow environment answers. The screenshot showed 13 sessions and 21.5M tokens, all from the local Mac environment, with no visible sign that the remote one (about $2.7k/day of OpenCode usage) was still loading. It is easy to read as the real total.

#26613 (open) would make repeat scans cheap: it uses the time_created index plus a persisted row cache, and its loop also covers session_message. The first scan would still parse every in-window row. #27679 (json_object extraction in SQL) would shrink each row before it reaches JS. #27667 is a different failure (OOM from huge user rows).

Impact

Major degradation or frequent failure

Version or commit

Server 0.1.6-nightly.20261030.3940 (Linux); desktop 0.1.6-nightly.20261031.3956 (macOS). usage.ts is unchanged on main since 8f26ed7.

Environment

NixOS server, OpenCode 2.2.5 provider (all usage in session_message); macOS desktop client connected to it over WebSocket.

Logs or stack traces

# server.trace.ndjson (remote server), one Usage page open
ws.rpc.server.getUsageSummary   23:56:21  278s  Success
UsageService.scanSummary        23:56:21  277s  Success
UsageService.collectDirs        23:56:21  276s  Success
openCodeUsageReader.scan        23:56:21  276s  Success
readOpenCodeUsage               23:56:21  276s  Success
UsageService.scanTranscriptDir  23:56:21  174s  Success   # claude/grok/codex, in parallel

# event-loop-stalls.ndjson during the scan
2026-10-31T23:57:04Z delayMaxMs=4331 utilization=1 rssMb=1313
2026-10-31T23:57:35Z delayMaxMs=2391 utilization=1 rssMb=1131
2026-10-31T23:59:08Z delayMaxMs=2355 utilization=1 rssMb=1507

# desktop client
EnvironmentRpc.request server.getUsageSummary  local env   82 ms
EnvironmentRpc.request server.getUsageSummary  remote env  278069 ms

Workaround

Wait about 5 minutes on the Usage page before reading the totals. There is no workaround for the repeated scan cost.

Model and harness: Claude Opus 5.5 in OpenCode.

Activity

  1. imadtg commented on Nov 7, 2026

    @imadtg

    Same reader, a much smaller database, and still minutes per summary: on a spinning disk the cost follows bytes read, not row count.

    Version: server 0.1.7-nightly.20261101.4016 (background service); current main (298cb24) has the same uncached query in packages/provider-opencode/src/server/usage.ts.
    Environment: Linux 7.2.8 (Omarchy), Node 24.22, OpenCode 2.1.4, 7.6 GB RAM, 5400 rpm SATA HDD under LUKS2 + Btrfs.

    Database: opencode.db is 453 MB but holds only about 8,000 message and 8,300 session_message rows (about 28 KB of data per row). The scan selects data for every in-window row, so with rows this large a summary can read most of the file from disk again.

    Server traces, 2026-11-04 21:41 to 2026-11-05 04:50. I excluded windows where other heavy disk jobs ran on this machine.

    runs median max
    UsageService.scanSummary 7 62 s 123 s
    openCodeUsageReader.scan within it 7 61 s 121 s

    The OpenCode reader dominates even in scans where everything else is cheap. At 04:27 it took 87.8 s, while the Antigravity reader took 0.5 s and the transcript walk 0.3 s.

    Knock-on effect. In 3 of the 7 scans, the Antigravity reader's 9 per-database reads took 45–86 s in total. In the other 4 they took 0.4–7 s. The databases have not changed since October, so their fingerprints match and those reads should be cache hits: one realpath and two stat calls each. The extra time looks like queueing behind other I/O in the same scan, which is the shape described in #26417, but I have not confirmed that.

    What would help here, in order:

    1. Cache the OpenCode result like the transcript and Antigravity readers already do. Key it on the highest rowid seen and time_updated, so a repeat request only reads new or updated rows.
    2. Select only the fields the summary needs instead of the whole data blob. Where the schema allows it, extract them in SQL, so a row's context is not read and parsed for its token counts.
    3. Keep the scan off the request path and return partial totals meanwhile, in line with the expected behaviour above, so a slow disk does not hold the Usage page and the RPC open.

    Related: #27667 (the same reader with unbounded page bytes), #26417 (filesystem work exhausting the libuv pool).

    Written by

    Claude Code (Claude Opus 5.5, claude-opus-5-5), following the t3 triage playbook, from measurements on the machine described above.

  2. nkoynov commented on Nov 11, 2026

    @nkoynov
    ContributorAuthor

    Still happens on 0.1.8-nightly.20261107.4340. #33612 (in 4446 and later) makes each scan cheaper but leaves it on the event loop.

    On 2026-11-11 one open of the Usage page in the desktop app started two summaries 8 s apart (two windows). Each ran its own readOpenCodeUsage over a 13.3 GB OpenCode 2 database: 2,151 s and 2,153 s, side by side. While they ran:

    • event-loop-stalls.ndjson showed utilization 1.0 in every sample (max delays 2.1–5.6 s), and RSS went from 0.6 to 1.1 GB.
    • 55 % of orchestrationV2.dispatch.withReceipt took over 1 s (p90 15 s, max 28.8 s), against 7 % (p90 0.7 s) outside the scans over the surrounding 2.5 days. Point queries on T3's own database waited 15–17 s for the connection.
    • The server's main thread was blocked on disk reads (D state) in 12 of 30 one-second samples. The OpenCode database is on a busy disk (about 2 ms per read).
    • The desktop app hit its 15 s ping timeout, reconnected through the relay, and moved back to the direct route two minutes later, re-subscribing everything each time.
    • The page's two requests were interrupted after about 10 minutes, but the scans run detached and kept going, so closing the page doesn't end the stall.

    The same happened on 2026-11-09 (two scans, 1,272 s and 1,278 s; 43 % of dispatches over 1 s). A second server with a 2 GB database ran 22 scans of up to 85 s each within six minutes on 2026-11-10.

    On 4515 the reader still opens NodeSqliteClient.layer({ filename, readonly: true }) without readerWorker, so every page is a synchronous node:sqlite call on the event loop. A 30-day window covers most of an active database (91 % of the 750k session_message rows here). Opening it with readerWorker: true, as T3's own database does, and piping sql.withTransaction(read) through NodeSqliteClient.readOnly would move the transaction and every statement in it to the worker (the transaction picks its connection when it starts, so the wrapper has to be outside it). The database is WAL, which the reader worker requires. If the worker can't start, the pool turns off and reads fall back to the event loop with only a warning in the log, so this reader may be better off refusing in that case.

    Workaround here: a preload that refuses session_message/message statements on read-only connections to opencode*.db. The reader then reports "Some OpenCode history could not be read." and the page shows the other sources.

    Model and harness: Claude Opus 5.5 in OpenCode, via T3 Code.

  3. juliusmarminge commented on Nov 13, 2026

    @juliusmarminge
    Member

    This is the same problem as #34897, so I'm closing it in favour of that one. If it is different, say how and it will be reopened.

  4. nkoynov commented on Nov 14, 2026

    @nkoynov
    ContributorAuthor

    Retested #34897 on nightly 4669 (db2cc20) with OpenCode 2.3.0. The stall part is fixed: during a 9.4 s cold scan of a 1.96 GB opencode.db, other HTTP requests answered in under 15 ms.

    The repeat part only partly is. The row cache's version is the database file's identity, and that includes the main file's size and mtime. Both change at every WAL checkpoint, so the first refresh after a write that reaches a checkpoint drops the whole cache and rereads every body. In the VM, a refresh with nothing changed took 0.9 s, and one after a single turn took 3.2 s. On a busy server that's most refreshes.

    Both tables here have time_updated and the per-row check already compares it with octet_length. So would it be enough to key the version on dev, inode and birth time only, to catch a replaced file, and let the per-row markers pick up new and changed rows?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions