Skip to content

Intermittent multi-second first-request stall on Bun serve under CPU load (node:http dispatch starvation) #46437

Closed as not planned

Description

@aman-kumar-gw

Description

On Bun, opencode serve intermittently hangs its first request for tens of seconds under CPU load, then recovers (startup-only; once warm it's fine).

I traced it end to end: the request-handler fiber is never forked — the TCP connection is accepted and the request bytes are read into userspace (rx_queue=0), but Bun's node:http never emits the JS request event while the loop is parked in epoll_pwait2, so Effect never sees the request. Bisected to Bun: 1.3.11 does not stall; 1.3.12/1.3.13/1.3.14 do (rebuilding opencode on 1.3.11 drops the rate from ~100% to ~5% under the same load) — so it's a Bun regression under load, not app logic. Effect's default scheduler flushes fiber dispatch via globalThis.setImmediate, which hits the same starvation.

Proposed fix in #46436 (route the scheduler flush through a MessageChannel macrotask).

OpenCode version

1.18.18 (Bun 1.3.14)

Steps to reproduce

  1. Run opencode serve on a CPU-constrained container (e.g. a 2 vCPU cgroup) under load.
  2. Fire the first request as soon as it's listening (a warm-up GET /session/:id is enough).
  3. Under heavy contention the first request hangs ~100% of the time for 30–60s before self-clearing; under lighter load it's intermittent (a few percent of cold boots).

Operating System

Debian 12 (bookworm) container, Linux 6.17 x86_64, cgroup-limited to 2 vCPU / 4 GiB, Docker + overlayfs.

Activity

  1. aman-kumar-gw commented on Sep 2, 2026

    @aman-kumar-gw
    Author

    Deterministic reproduction (no CPU load needed)

    The trigger is a request that arrives in the window between the socket starting to accept connections and node:http dispatch being wired. Firing the first request the instant the port accepts reproduces it 100% of the time; a ~0.5s pause after listen avoids it every time. CPU load only widens that window (which is why it looks intermittent in a real deployment), it is not required.

    Environment verified: Debian 12 (bookworm), cgroup-limited 2 vCPU (cpu.max = 200000 100000), overlayfs (sysbox container), Linux 6.8 x86_64, official opencode 1.18.18 release binary (opencode-linux-x64.tar.gz, embeds Bun 1.3.14). Only bash + curl are needed.

    # 1. exact official artifact
    cd /tmp && curl -fsSL -o oc.tgz https://github.com/anomalyco/opencode/releases/download/v1.18.18/opencode-linux-x64.tar.gz
    mkdir -p oc && tar -xzf oc.tgz -C oc && B=$(find oc -name opencode -type f | head -1) && chmod +x "$B"
    "$B" --version   # 1.18.18
    
    # 2. one trial: start serve, fire the first request the INSTANT the port accepts, time it
    PW=x
    trial () { # $1=delay after listen (s)  $2=curl timeout (s)
      rm -rf ~/.local/share/opencode ~/.local/state/opencode/locks 2>/dev/null
      OPENCODE_SERVER_PASSWORD=$PW "$B" serve --port 4096 >/tmp/oc.log 2>&1 </dev/null &
      sp=$!
      for i in $(seq 1 1200); do (echo >/dev/tcp/127.0.0.1/4096) 2>/dev/null && break; sleep 0.05; done
      sleep "$1"
      r=$(curl -s -m "$2" -o /dev/null -w '%{http_code} %{time_total}' -u "opencode:$PW" http://127.0.0.1:4096/session/ses_warmup)
      r2=$(curl -s -m 20 -o /dev/null -w '%{http_code} %{time_total}' -u "opencode:$PW" http://127.0.0.1:4096/session/ses_warmup)
      echo "delay=${1}s  first: http=${r%% *} t=${r#* }s   | next request: http=${r2%% *} t=${r2#* }s"
      kill -9 $sp 2>/dev/null; wait 2>/dev/null; sleep 1
    }
    
    # 3. reproduce: immediate fire -> the first request never gets a response
    for n in 1 2 3; do trial 0 60; done
    
    # 4. control: same thing with a 0.5s pause after listen -> fast every time
    for n in 1 2 3; do trial 0.5 30; done

    Results (9 immediate-fire trials, all stalled; 3 delayed, all fast)

    condition first request trials
    immediate fire, no load, cold page cache 000 @ 60s (hung) 1/1
    immediate fire, no load, warm page cache 000 @ 60s (hung) 2/2
    immediate fire, 4× yes hogs on 2 vCPU 000 @ 60s (hung) 4/4
    immediate fire, 2× yes hogs 000 @ 60s (hung) 2/2
    immediate fire, 150s timeout 000 @ 150s — does not self-clear 1/1
    0.5s pause after listen 404 in 0.05–0.13s 2/2
    2s pause after listen 404 in 0.11s 1/1

    Two corrections to the current description, from the data:

    1. The stalled request never clears. With a 150s client timeout it is still hung at 150s; it is not a 30–60s delay. What looks like "self-clears after ~60s" in a deployment is the client (curl -m 60) giving up. The connection is orphaned: bytes are accepted but the request event is never emitted for it.
    2. The server is otherwise healthy. The very next request on the same process is served in ~0.1–0.2s. So it's a lost dispatch for the first connection, not a frozen event loop.

    Page-cache state (cold vs warm binary) and CPU load made no difference to the hit rate — the only variable that matters is whether the first request lands inside the startup window.

  2. axsuul commented on Oct 1, 2026

    @axsuul

    confirming the deterministic repro on 1.18.33 and 1.18.34 (Linux x86_64, no CPU limit, 4 to 8 free cores). Measured from the first TCP accept: GET /global/health at +0, +50, +100, +150, +200 ms never gets a response, at +250 ms and later it returns 200 in about 5 ms. The listening on line prints at about +250 ms, so the window is the gap between bind and the handler being wired. 3 of 3 runs on each version.

    Would be great to get #46436 or an equivalent reviewed. The stalled connection never clears, so any client that connects right after bind and retries with backoff can land in the window and hang for its full timeout.

  3. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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

Metadata

Metadata

Assignees

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