Skip to content

service: hardcoded default port 49374 locks out any second client on shared loopback (multi-user/WSL mirrored), surfacing only a generic timeout #50672

Open

Description

@yveming

Summary

The managed background service binds a hardcoded default port (49374 for
latest/dev/beta/next channels). Service registration
(~/.local/state/opencode/service.json) is per-user, but the port is
machine-global on any shared loopback. Any second client on the same machine —
another Unix user, the Windows host under WSL2 networkingMode=mirrored, or a
--network host container — cannot bind the port. Instead of failing fast with
the real cause, the client spawns serve --service contenders for ~2 minutes
and then reports only Timed out waiting for the background service to start.
The actionable message exists only in the log file, and per the residual gap in
#41696 it can be swallowed entirely by contender overlap.

Environment

Both installations are v2.0.14, channel latest.

Environment A (holds the port):

  • opencode version: 2.0.14
  • OS: Linux 6.18.33.2-microsoft-standard-WSL2, networkingMode=Mirrored in .wslconfig
  • Terminal: TERM=xterm-256color
  • Shell: /bin/bash
  • Install/channel: latest
  • Active plugins: none found in config

Environment B (fails to start):

  • opencode version: 2.0.14
  • OS: Windows host of the WSL2 above (mirrored mode shares one loopback)
  • Terminal: cmd.exe (TERM unavailable: Windows console)
  • Shell: cmd
  • Install/channel: latest
  • Active plugins: none found in config

Reproduction

  1. In WSL2 with networkingMode=Mirrored, start opencode once and exit the
    TUI. The background service (opencode serve --service) persists, listening
    on 127.0.0.1:49374.
  2. On the Windows host (same loopback), run opencode.
  3. Wait ~2 minutes.

Same lockout without WSL: on a shared Linux box, run opencode as user A, exit,
then run opencode as user B.

Expected Behavior

Either:

  • the default service port is not a machine-global constant — pick an
    ephemeral/incremental port and record it in the registration file (it already
    stores url), or
  • at minimum, fail fast with the real cause and identify the occupying process.

Actual Behavior

Client spawns contenders in a loop, one failure every ~10s for ~2 minutes:

timestamp=2026-09-22T14:47:48.025Z level=ERROR message="cli process failed"
cause="Cause([Fail(Error: Managed service port 49374 on 127.0.0.1 is already in
use by another process. Configure another port with `opencode service set port
<port>` and start the service again. (cause: ServeError (cause: Error: Failed to
start server. Is port 49374 in use?)))])" args="[\"serve\",\"--service\"]" role=cli

(20+ identical repeats), then the only thing the TUI shows:

Error: Timed out waiting for the background service to start
    at service.ensure ...
    at cli.server-connection.resolve ...

Occupying process, from the WSL side:

$ ss -tlnp | grep 49374
LISTEN 0 512 127.0.0.1:49374 users:(("opencode",pid=12566,fd=10))

$ ps -p 12566
PID    STARTED              ELAPSED  CMD
12566  Tue Sep 22 21:46:33  01:14:39 /home/<user>/.opencode/bin/opencode serve --service

Additional Context

The port is a hardcoded constant (extracted from the v2.0.14 binary):

if (e==="latest"||e==="dev"||e==="beta"||e==="next") return 49374;
if (e==="local") return 49375;
return 1e4 + ...

All four release channels share 49374, for every user on the machine.

Collision matrix (fixed default port × shared loopback):

Scenario Collides
WSL2 mirrored ↔ Windows host yes (this report)
Two Unix users, same machine yes
--network host container ↔ host yes
-p 49374:... published container ↔ host yes
WSL2 NAT default / Docker bridge / full VM no

Multi-user impact is a silent lockout, not a data leak. Verified: API
returns 401 unauthenticated; service.json (registration + password) is 0600.
But user B's registration lives in B's home, so B cannot discover A's service;
B gets a bare timeout with no hint that another account is the cause, and the
remediation (opencode service set port) appears only in the log.

Warning on fix direction: #7629 proposes "probe the port and reuse the
existing service." That must not be applied to the default managed port across
users/environments. The service owns sessions, provider credentials,
permissions, and tool execution, so reuse keyed on port occupancy would
(a) require sharing the 0600 password across users or un-authenticating the
API, and (b) bind client A's in-flight work to client B's process lifecycle —
B logging out, wsl --shutdown, or a service auto-update restart (cf. #38567)
would kill A's running tasks with zero control on A's side. Reuse is only safe
when keyed on the client's own registration file (stale-service recovery),
never on port occupancy.

Related: #41696 + PR #41793 addressed surfacing stderr, but the follow-up
comment on #41696 documents a remaining contender-overlap gap that discards the
captured failure until the overall timeout — exactly what I observe on 2.0.14.
#19272 / #21170 / #46263 / #10357 cover adjacent symptom/solution ground; this
issue is the root cause: fixed machine-global default port + per-user
registration.

Workarounds: none applied on side B (user only retried). Side A unaffected.

Activity

  1. github-actions commented on Sep 22, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Your issue is more specific than any of these — it focuses on the machine-global fixed port as root cause for multi-user lockout and includes a useful warning about the "reuse by port occupancy" anti-pattern — so it may still warrant its own tracking. But linking these in your description (as you've already done for some) will help maintainers triage.

  2. yveming commented on Sep 22, 2026

    @yveming
    Author

    Thanks for the triage. Adding context on the linked issues to help with review:

    No further action needed from the bot; leaving this open for the root-cause discussion (fixed port + per-user registration, and the warning against fixing it via "reuse by port occupancy").

  3. p7gg commented on Sep 23, 2026

    @p7gg

    NAT mode also collides (not just networkingMode=mirrored) — and the Desktop app dies with a fatal renderer error instead of a TUI timeout

    Still present on 2.0.15. Environment:

    • Windows 11 (build 26100) + OpenCode Desktop 2.0.15 (win-x64, packaged; first hit on 2.0.14 right before auto-update)
    • WSL2 archlinux, WSL 2.7.14.0 / kernel 6.18.33.2-2, default NAT networking (.wslconfig has no networkingMode, no per-distro override)
    • WSL-side CLI 2.0.15

    This contradicts the collision matrix row WSL2 NAT default … | no: NAT still collides because WSL2 localhost forwarding binds the Windows-side loopback port via wslrelay.exe.

    Reproduction

    1. In WSL2 (NAT), start opencode once. The shared service persists on 127.0.0.1:49374.
    2. On the Windows host, launch OpenCode Desktop.
    3. The desktop's bundled CLI spawns serve --service contenders on the same default port; each fails immediately. After ~120 s the renderer throws a fatal error and the UI is unusable.

    Evidence

    WSL side:

    $ ss -tlnp | grep 49374
    LISTEN 0 512 127.0.0.1:49374 users:(("opencode",pid=6669,fd=12))   # opencode serve --service
    

    Windows side — the NAT forwarder owns the Windows loopback port:

    $ netstat -ano | grep 49374
    TCP 127.0.0.1:49374 LISTENING 23296   # wslrelay.exe
    

    Windows service log — 99 identical failures between 12:34Z and 12:55Z (log capture end), one every ~5–10 s:

    ERROR "cli process failed" cause="...Managed service port 49374 on 127.0.0.1 is already in use by another process..."
    

    Desktop renderer.log — fatal, unlike the CLI's timeout message:

    Desktop IPC handler failed
    Caused by: "Timed out waiting for the background service to start"
    

    Extra wrinkle: after the host slept overnight, the Windows-side registration pointed at a dead PID (2.0.14), so the app classified the service as missing and tried to start a fresh one — straight into the collision.

    Workaround (from the error message, verified): on Windows, opencode-cli.exe service set port 49375 + service start, then relaunch the app. WSL side untouched on 49374.

  4. ibaciu6 commented on Sep 26, 2026

    @ibaciu6

    Confirming this on 2.0.18, and the affected client is the desktop app, not the TUI. Also: the collision matrix understates the blast radius — WSL2 networkingMode=nat with localhostForwarding=true (the default) collides too.

    Still present on 2.0.18, via the desktop app

    Same root cause, three additions worth folding in.

    1. The OpenCode Desktop (Electron) client fails the same way, and the real error never surfaces to the UI. The desktop spawns its own bundled opencode-cli.exe serve --service and retries in the same loop, but the only thing the user ever sees is a fatal renderer error:

    Error: Desktop IPC handler failed
    Caused by:
    { "name": "Error", "message": "Timed out waiting for the background service to start" }
    

    The actionable Managed service port 49374 ... is already in use text stays in the log. A desktop user has no opencode service set port in their workflow, so this reads as "the app is just broken" rather than a port conflict.

    2. The collision matrix is wrong for NAT mode. This row says NAT does not collide:

    | WSL2 NAT default / Docker bridge / full VM | no |

    With networkingMode=nat and localhostForwarding=true, WSL2 relays a WSL 127.0.0.1 listener to Windows 127.0.0.1 via wslrelay.exe, so the two do share that loopback. My .wslconfig:

    [wsl2]
    networkingMode=nat
    dnsTunneling=true
    firewall=true
    autoProxy=true
    localhostForwarding=true

    The WSL service and the Windows desktop each believed they owned the port. From Windows:

    $ Get-NetTCPConnection -LocalPort 49374 -State Listen
    LocalAddress LocalPort State  OwningProcess
    127.0.0.1    49374     Listen 9296
    
    $ Get-CimInstance Win32_Process -Filter 'ProcessId=9296'
    Name        CommandLine
    wslrelay.exe  --mode 1 --vm-id {4d744808-...} --handle 1712
    

    wslrelay.exe is the occupier, and its existence is the tell that this is WSL2 rather than a genuine second opencode client. From the WSL side the owner looks like an ordinary opencode process:

    $ ss -tlnp | grep 49374
    LISTEN 0 512 127.0.0.1:49374 users:(("opencode",pid=4771,fd=10))
    
    $ ps -p 4771 -o args=
    /home/<user>/.opencode/bin/opencode serve --service
    

    So this is not a narrow mirrored-mode edge case — it is the stock WSL2 configuration, which is the common setup.

    3. Loop shape on 2.0.18. 25 cli starting entries and 31 port-conflict failures across two desktop launches, ~10s apart, then the 120s timeout:

    timestamp=2026-09-26T03:35:32.395Z level=INFO  message="cli starting" version=2.0.18 channel=latest local=false args="[\"serve\",\"--service\"]" role=cli
    timestamp=2026-09-26T03:35:47.738Z level=ERROR message="cli process failed"
      cause="Cause([Fail(Error: Managed service port 49374 on 127.0.0.1 is already in use by another process.
      Configure another port with `opencode service set port <port>` and start the service again.
      (cause: ServeError (cause: Error: Failed to start server. Is port 49374 in use?)))])"
    

    Verified workaround

    Move the Windows side off 49374, leaving WSL untouched. Moving WSL instead is the wrong direction here: the WSL service stays resident, and it is what feeds the loopback relay.

    opencode service set port 49375
    

    Writes C:\Users\<user>\.config\opencode\service.json → { "port": 49375 }. Note the desktop's own service get was {} before this — the port was never configured on either side, both were silently taking the default.

    Confirmed after the change:

    v2 CLI background service ready { version: '2.0.18', probed: true, url: 'http://127.0.0.1:49375', port: '49375' }
    

    Service ready in ~25ms instead of timing out at 120s, no renderer.log produced at all, and both ports coexist (49374 WSL relay + 49375 Windows). Manually starting the CLI also works and serves the UI:

    $ opencode-cli.exe serve --service
    server listening on http://127.0.0.1:49375
    

    The hardcoded-default extraction in this issue still holds on 2.0.18 — 49374 appears as a string in the shipped opencode-cli.exe, and the desktop's app.asar does not hardcode it, so the client does follow the configured port. That makes the one-line config change a sufficient fix.

    Secondary: the desktop debug export ships a stale server log

    Worth a separate issue if it is not already tracked, because it actively misleads triage. "Export debug logs" produced a bundle whose server-1/opencode.log was a weeks-old log from a previous working run (dated ~7 weeks before the failure), so the bundle contained no trace of the actual failure. The current run's CLI log was in a different location and had to be found separately:

    serverLogs:
      C:\Users\<user>\.local\share\opencode\log
      C:\Users\<user>\AppData\Roaming\ai.opencode.desktop\opencode\log
    

    The per-run desktop logs (main.log, renderer.log) were correct; only the server-1/ copy was stale. Anyone triaging from the zip alone would see a clean startup and conclude the cause was elsewhere.

    Environment

    • opencode version: 2.0.18 (OpenCode Desktop 2.0.18, bundled CLI 2.0.18), channel latest
    • OS: Windows 11 Pro 10.0.26200 x64, WSL2 2.7.13.0, kernel 6.18.33.2-microsoft-standard-WSL2
    • WSL networking: networkingMode=nat, localhostForwarding=true
    • Terminal: cmd.exe (Windows side); TERM=xterm-256color (WSL side)
    • Shell: cmd / powershell (Windows side); /bin/bash (WSL side)
    • Install/channel: packaged desktop build, latest
    • Active plugins: none configured
    • Reproducibility: 100% while the WSL service holds 49374; resolves immediately on reassigning the Windows port

    Username, machine name, VM GUID, service password, and window identifiers redacted.

  5. alint77 commented on Oct 4, 2026

    @alint77

    Still present on 2.0.22 (Linux aarch64). Hit it on a shared multi-user HPC login node: another Unix user's opencode serve --service held 127.0.0.1:49374, so mine failed with the generic "Timed out waiting for the background service to start" after ~2 min. The port-in-use cause was only in ~/.local/share/opencode/log/opencode.log. Workaround: opencode service set port <free port>.

  6. Shra1V32 commented on Oct 13, 2026

    @Shra1V32

    Adding context on multi-tenant / multi-user Linux hosts and how this connects to the open fixes:

    1. Per-user default port via process.getuid?.() (ServiceConfig.defaultPort in packages/cli/src/services/service-config.ts):

      • Right now, fix(cli): give Windows and WSL their own service default ports #54988 hashes WSL_DISTRO_NAME into defaultPort() (hashedPort(${filename(channel)}:${distro})) so WSL distros sharing loopback don't collide on 0xC0DE (49374), while keeping the port bind as the per-database single-instance lock (fix(cli): elect managed service by port bind #37572).
      • Multi-user POSIX hosts (and multiple users inside the same WSL distro) have the exact same topology: service.json and opencode.db are per-user (XDG_STATE_HOME / XDG_DATA_HOME), while 127.0.0.1 is shared across all UIDs in the network namespace.
      • Folding process.getuid?.() into the defaultPort calculation alongside distro is ideal because the numeric UID is kernel-enforced, stays consistent across all shells/sessions for that user (unlike env vars), and guarantees every contender for the same user still competes for the exact same deterministic port while different OS users on the machine get distinct ports automatically.
    2. Secondary multi-user collision & security surface on /tmp/opencode (packages/util/src/global-roots.ts & packages/util/src/global.ts):

  7. mathomp4 commented on Oct 15, 2026

    @mathomp4

    I'm commenting here because I think this might be related to my issue I'm having.

    I was looking to upgrade to opencode v2.0.29 and hit this on a shared multi-user HPC (Linux, not WSL) login node. Another user's opencode serve --service already held 127.0.0.1:49374, so mine failed with:

    Managed service port 49374 on 127.0.0.1 is already in use by another process.
    

    On a cluster, many users share each login node, so this will be common. Every user gets the same default port, but the registration in ~/.local/state/opencode/service.json is per user. Nobody else can start a service on that node until the first user's service exits.

    Workaround: each user has to run a one-time opencode service set port <port>. A per-user port works, for example $((20000 + $(id -u) % 40000)). Doing this for every user on a site-wide install is awkward. I couldn't find an environment variable that overrides the port, and there is no site-level default I can set in a shared install or modulefile.

    Suggestions (in the order I'd prefer them):

    1. Fold the UID into ServiceConfig.defaultPort, as @Shra1V32 proposed above. That would fix multi-user hosts without any configuration.
    2. Add an environment variable such as OPENCODE_SERVICE_PORT, so administrators can set it from a modulefile or a system profile. (Which I think I can make programmatic since it's lua modules...)
    3. Fail fast with the real cause in the TUI, instead of the generic 2-minute timeout.
  8. mathomp4 commented on Oct 15, 2026

    @mathomp4

    @yveming I wonder: should we make the "title" of this issue more broad now? Since both @Shra1V32 and myself hit this on "plain" Linux (compared to WSL)

  9. yveming commented on Oct 15, 2026

    @yveming
    Author

    The title contains 'multi-user' and in the section 'Additional Context' listed a scenario 'Two Unix users, same machine'.

    22. @yveming I wonder: should we make the "title" of this issue more broad now? Since both @Shra1V32 and myself hit this on "plain" Linux (compared to WSL)

  10. mathomp4 commented on Oct 15, 2026

    @mathomp4

    @yveming Sounds good. Fingers crossed this gets fixed soon! 🙂

  11. mathomp4 commented on Oct 19, 2026

    @mathomp4

    Workaround: each user has to run a one-time opencode service set port <port>. A per-user port works, for example $((20000 + $(id -u) % 40000)). Doing this for every user on a site-wide install is awkward. I couldn't find an environment variable that overrides the port, and there is no site-level default I can set in a shared install or modulefile.

    Replying to myself in case I forget, but this is doable in LMod with an execute statement to have each user set the port on load. Seems to work on a test machine.

    Of course, would be good to not need this "hack".

  12. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed write-up and the collision matrix! Here's where each part stands:

    What's left is your main request: making the default managed port per-user, or ephemeral and recorded in service.json, so that two Unix users on one machine, or a --network host container and its host, don't compete for the same port on Linux. The fixed per-channel port is currently a deliberate default, with opencode service set port <port> as the override, so I've filed this as a feature request. Maintainers will consider it, along with your warning that a service should only be reused through the client's own registration, never just because its port is taken.

    Related issues:

  13. gparadis commented on Nov 5, 2026

    @gparadis

    Still reproducible on v2.2.5 (latest, fresh curl -fsSL https://opencode.ai/v2/install | bash) on a shared Linux JupyterHub host.

    Two Unix users on the same host:

    • User A's managed service is listening on 127.0.0.1:49374 (opencode serve --service, up for days).
    • User B has no ~/.config/opencode/service.json, runs opencode, and gets the port-in-use error after ~15s. No service starts.

    User B's log:

    channel=latest ... spawning process command=lsof args=["-nP","-iTCP:49374","-sTCP:LISTEN","-Fpn"]
    ERROR ... Managed service port 49374 on 127.0.0.1 is already in use by another process
    

    Verified workaround:

    opencode service set port $((20000 + $(id -u) % 10000))
    opencode service start

    The client then connects and opencode models works. opencode --standalone is a zero-config alternative that uses a private server instead of the managed service.

    One extra data point for moving the Linux default: 49374 is inside the default Linux ephemeral range (net.ipv4.ip_local_port_range = 32768 60999), so an unrelated outbound socket can occupy it at startup too. The Windows/WSL path already remaps default ports >=32768; Linux still uses 49374 unchanged.

    +1 to folding the UID into defaultPort (or using an ephemeral port persisted in service.json).

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