Skip to content

App leaks one app.db file descriptor (+~2 MB RSS) per instance-watcher event, eventually killing the WebKit renderer #7371

Description

@lbellows

Please confirm the following.

  • I checked the existing issues for duplicate problems
  • I have tried resolving the issue using the support portal
  • I have ensured my Modrinth App installation is up to date

What version of the Modrinth App are you using?

Modrinth App 0.19.1 (Flathub com.modrinth.ModrinthApp, system install) — Ubuntu 24.04, X11/XFCE, 28 GiB RAM

What operating systems are you seeing the problem on?

Linux

Describe the bug

The app leaks one open file descriptor on app.db (plus ~2 MB of RSS) per instance-watcher event. On a large library this accumulates into a multi-GB launcher process, then collapses into a self-sustaining error storm that kills the WebKit renderer — leaving a grey, unresponsive window whose native titlebar still works, so the user can right-click → Close it.

This is measured, not inferred. I have a clean control window at the end.

1. The fd leak

Live process, 3h48m into a session:

$ ls -l /proc/<pid>/fd | awk '{print $NF}' | sort | uniq -c | sort -rn | head
   1673 .../ModrinthApp/app.db
    100 .../ModrinthApp/app.db-wal
     11 anon_inode:[eventfd]
      9 /dev/dri/renderD128
      5 anon_inode:[timerfd]

1,774 open descriptors on the SQLite database. The process fd limit is 1048576, so this is not EMFILE — nothing forces a cleanup, it just grows.

RSS at that same moment was 3,683 MiB. That works out to 2.08 MB per leaked descriptor, which is exactly SQLite's default per-connection page cache (cache_size = -2000, i.e. 2000 KiB). So each leaked fd is a whole live connection carrying its own page cache, not a bare descriptor.

Note that 1,774 is far beyond any single sqlx pool's max_connections, which suggests connections (or pools) are being created per-event and dropped without being closed, rather than one pool overflowing.

2. The trigger is the instance file watcher

theseus::state::instances::watcher fires roughly once a second the whole time Minecraft is running:

INFO init_watcher: theseus::state::instances::watcher: World updated: /home/<user>/.var/app/com.modrinth.ModrinthApp/data/ModrinthApp/profiles/<instance>/saves/<world>/level.dat

This library is large — 47 profiles, 7,504 directories, 26 GB — which is presumably why it shows up here and not on a typical install.

3. Control: the leak is per-event, not per-unit-time

Same process, same session, sampled once a minute for 7 minutes while the user was idle (game still running, but no world-save events):

11:55:39 fds=1835 rss=3683MiB
11:56:39 fds=1834 rss=3683MiB
11:57:39 fds=1834 rss=3684MiB
11:58:39 fds=1833 rss=3684MiB
11:59:39 fds=1834 rss=3682MiB
12:00:39 fds=1834 rss=3683MiB
12:01:39 fds=1833 rss=3683MiB

Completely flat — and the launcher log has zero lines of any kind in that window. Compare the active part of the same session:

Window Watcher events fd growth
08:06 → 10:15 (playing) ~1/sec +1,774
11:55 → 12:02 (idle) none 0

So it leaks at roughly 830 fds/hour while actively playing and zero when idle. Leaving the app open overnight is harmless; an 8-hour play session is what kills it.

4. Collapse and renderer death

Once it saturates, the database starts failing, in this order:

ERROR theseus::state::instances::watcher: Failed to sync instance content after filesystem change:
  Error interacting with database: error returned from database: (code: 5) database is locked
  ... then: (code: 14) unable to open database file
  ... then: pool timed out while waiting for an open connection

Every one of these is pushed to the frontend by theseus_gui::error from packages/app-lib/src/api/instance/get.rs:17. Measured rate, counting log lines per second:

   4193 2026-08-29T10:31:59
   4748 2026-08-29T10:32:00
   3919 2026-08-29T10:32:01
   3788 2026-08-29T10:32:02
   4323 2026-08-29T10:32:03

~4,000–5,000 errors per second. Totals across this machine's logs: 1,390,704 (code: 14) unable to open database file lines, 338k errors inside one 2-minute window, a single 189 MB session log, and 1.1 GB in launcher_logs/ overall.

Feeding that many error events into the WebView drives WebKitWebProcess to 11–15 GiB RSS, at which point it either gets OOM-killed or crashes on its own. Either way the Tauri window frame survives the renderer, which is the grey-but-closable window.

Elapsed time from first database error to dead renderer is consistently about two minutes:

Session start First DB error Renderer death
Aug 29 08:03 10:29:14 OOM-killed 10:31:36 — WebKitWebProcess 11,774 MiB
Aug 29 15:57 Aug 30 06:24:56 OOM-killed 06:27:08 — WebKit 13,605 MiB + launcher 12,478 MiB
Aug 27 15:53 OOM-killed 15:50:13 — WebKitWebProcess 14,715 MiB
Aug 25 / 26 / 28 (×2) self-crash, coredumpctl shows SIGABRT / SIGTRAP in WebKitWebProcess

That is roughly once a day for this user.

Steps to reproduce

  1. Create a library with a large number of instances (47 here; the effect should scale with instance and directory count).
  2. Launch any instance and play for several hours, so the watcher sees continuous level.dat writes.
  3. Watch the launcher process: watch -n60 'ls /proc/$(pgrep -f modrinth-app-wrapped)/fd | wc -l'
  4. The count climbs monotonically and never drops. RSS tracks it at ~2 MB per fd.
  5. Eventually the database errors begin and the window goes grey within ~2 minutes.

Expected behavior

Each watcher-driven database interaction should release its connection when finished, so the open-fd count on app.db stays bounded (at the pool's max_connections) regardless of session length or library size.

Separately — and worth fixing independently, since it is what turns a slow leak into a hard failure — the frontend error path should rate-limit or coalesce identical errors. A failure mode that emits 4,000 identical errors per second into the WebView will take the renderer down no matter what the underlying cause is.

Additional context

Possibly related, though I think both are downstream of different causes: #3414 (long-running memory growth, never root-caused) and #7276 (WebKitWebProcess CPU/memory loop on Linux). This report is specifically about the app.db descriptor leak, which I haven't seen described anywhere.

Happy to pull further diagnostics from this machine — it reproduces daily. Paths above are redacted; otherwise everything is verbatim.

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