Skip to content

Per-turn Workers leak an event loop and ~0.8 MB each on Bun < 1.4 (OpenCode V1 up to 1.19.3) #776

Open

Description

@moynzzz

Environment

  • Magic Context 0.49.4, OpenCode 1.19.1 (V1) built with Bun 1.3.14 (upstream pins bun@1.3.14 through 1.19.3), Linux x64 (WSL2)

What

Every short-lived Worker Magic Context starts and terminates leaks its event loop on Bun 1.3.14: 3 file descriptors ([eventpoll], [eventfd], [timerfd]) plus about 0.7–0.85 MB of native memory. GC never frees either. We measured about four per turn. The Workers the bundle creates per job are auto-search, the message worker, compaction-marker candidates and session warmup.

Measured

  • 15 long-running opencode serve processes (0.2–31 h old) held 26,404 leaked loops. Across processes, native memory (unnamed anonymous mappings) grew 0.65–0.88 MB per leaked loop. Example: one 31 h server had 8,113 leaked loops and 5.3 GB of native memory, out of 7.1 GB total RSS + swap.
  • Controlled test on one server: 15 one-line turns with no tools gave +61 leaked loops and +52 MB of native memory, measured after Bun.gc(true).
  • The same 15 turns on the same OpenCode code rebuilt with Bun 1.4.2: 0 leaked loops, +15 MB.

Count per process: ls -l /proc/<pid>/fd | grep -c eventpoll.

Repro (Bun only)

// worker.js: self.onmessage = (e) => self.postMessage(e.data)
import { readdirSync, readlinkSync } from "fs"
const loops = () => readdirSync("/proc/self/fd").filter((f) => { try { return readlinkSync(`/proc/self/fd/${f}`).includes("eventpoll") } catch { return false } }).length
for (let i = 0; i < 200; i++) {
  const w = new Worker(new URL("./worker.js", import.meta.url).href)
  await new Promise((r) => { w.onmessage = r; w.postMessage(i) })
  w.terminate()
}
console.log("eventpoll fds:", loops()) // Bun 1.3.14: 201, Bun 1.4.2: 1

Suggestion

Keep these Workers alive and reuse them instead of creating one per job. Bun's own advice is not to terminate Workers (oven-sh/bun#15964). Alternatively, document that OpenCode builds on Bun < 1.4 (V1 up to 1.19.3) leak memory on every turn. OpenCode 1.19.4 pins bun@1.4.4; we have not tested that release.

Activity

  1. magic-alfonso commented on Oct 24, 2026

    @magic-alfonso

    Thanks @moynzzz, this is a clear report, and the Bun-only repro with the 1.3.14 vs 1.4.2 comparison pins it down.

    Until we change anything, the workaround is in your last line: OpenCode 1.19.4 and later ship with Bun 1.4.4, which no longer leaks a terminated Worker's event loop, so upgrading OpenCode should stop the growth. If you try it, the eventpoll count from your repro would confirm it.

    Separately, you're on Magic Context 0.49.4, and the current release is 0.49.20. Its worker handling has changed since then (workers now close their own database handle before they're stopped), though they're still started per job, so it doesn't remove this leak on Bun 1.3.x.

    We're in the middle of a larger internal change, so reusing these workers instead of starting one per job is queued rather than started. We'll post here when we pick it up.

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