windmill-labs/windmill · warning

Port ${requested} is already in use${holderHint}. Using port

Error message

Port ${requested} is already in use${holderHint}. Using port ${port} instead.

What it means

This is a warning (not a thrown error) emitted by `wmill pipeline dev` in cli/src/commands/pipeline/dev.ts. When the requested port is already bound, resolveBindPort picks the next free port and logs which port was requested, who held it (holderHint), and which port is actually used. It exists so the dev server still starts instead of failing with EADDRINUSE.

Source

Thrown at cli/src/commands/pipeline/dev.ts:201

  watcher.on("change", (_ev, filename) => {
    if (filename && filename.toString().endsWith(".lock")) return;
    if (timer) clearTimeout(timer);
    timer = setTimeout(async () => {
      timer = undefined;
      try {
        current = await buildBundle();
        log.info(colors.cyan(`↻ rebuilt graph (${current.scripts.length} scripts)`));
        broadcast();
      } catch (e: any) {
        log.error(colors.red(`Failed to rebuild pipeline graph: ${e.message}`));
      }
    }, 150);
  });
  watcher.on("error", (e) => log.error(colors.red(`Watcher error: ${e.message}`)));

  const port = await resolveBindPort(opts.port ?? PORT, "wmill pipeline dev", {
    info: (m) => log.info(m),
    warn: (m) => log.warn(m),
  });

  const server = http.createServer((_req, res) => {
    res.writeHead(200);
    res.end();
  });
  // Loopback bind keeps the LAN out, but any browser tab can still open a
  // `ws://localhost:<port>` connection — and each frame ships the folder's full
  // script source. Gate the upgrade on an unguessable per-session token (carried
  // in the dev-page URL) so a stray page on the predictable dev port can't
  // exfiltrate the source. base64url → safe as a query value.
  // Stable across restarts (scoped to remote+workspace+root+folder+port) so an
  // already-open page reconnects after a CLI restart (see stableWsToken), not
  // just after a transient WS drop.
  const wsToken = await stableWsToken(
    workspace.remote,
    workspace.workspaceId,
    root,

View on GitHub (pinned to e474e8803c)

Solutions

  1. Check the log line: it already tells you the port actually used — just open/use that port instead.
  2. Find and stop the process holding the requested port (e.g. lsof -i :<port> or ss -ltnp) then rerun the command.
  3. Pass an explicit free port with --port <n> to make the assignment deterministic.
  4. If a stale `wmill pipeline dev` is the holder, kill it (it also holds the WebSocket the UI connects to).

Example fix

// before (deterministic port request that collides)
wmill pipeline dev --port 4545
// after
lsof -ti :4545 | xargs kill   # free the port first
wmill pipeline dev --port 4545
Defensive patterns

Strategy: validation

Validate before calling

// check the port before invoking the command
const net = require('net');
function portFree(port) {
  return new Promise(res => {
    const s = net.createServer();
    s.once('error', () => res(false));
    s.listen(port, () => s.close(() => res(true)));
  });
}
if (!(await portFree(4545))) console.warn('port 4545 busy; pick another');

Prevention

When it happens

Trigger: Running `wmill pipeline dev` (optionally with --port) while another process — often a previous `wmill pipeline dev` that never shut down, another wmill dev command, or any dev server — is already listening on the requested port (default PORT constant).

Common situations: A stale pipeline dev server left running in another terminal; two worktrees running dev servers simultaneously; a crashed session that didn't receive SIGINT; a hardcoded --port colliding with a dockerized service.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/b9a7eab2a6e09b1b. Report an issue: GitHub.