thedotmack/claude-mem · warning

Port did not free up after shutdown

Error message

Port did not free up after shutdown

What it means

During `claude-mem stop`, the CLI sends the HTTP shutdown request and then waits (about 15s, platform-adjusted) for the port to become free. If the listener socket is still bound after that window, this warning fires; the CLI still removes the PID file if it owns it and exits 0.

Source

Thrown at src/services/worker-service.ts:1101

    case 'start': {
      const result = await ensureWorkerStarted(port);
      if (result === 'dead') {
        exitWithStatus('error', 'Failed to start worker');
      } else {
        exitWithStatus('ready', result === 'warming' ? 'Worker started; still warming up' : undefined);
      }
      break;
    }

    case 'stop': {
      // Capture the dying worker's pid BEFORE shutdown so the PID-file
      // cleanup below can prove it deletes THAT worker's file (or a dead
      // pid's leftover) — never a live successor's (Phase 5).
      const stoppedPid = await getCurrentWorkerPid(port, 2000);
      await httpShutdown(port);
      const freed = await waitForPortFree(port, getPlatformTimeout(15000));
      if (!freed) {
        logger.warn('SYSTEM', 'Port did not free up after shutdown', { port });
      }
      removePidFileIfOwner(stoppedPid);
      logger.info('SYSTEM', 'Worker stopped successfully');
      process.exit(0);
      break;
    }

    case 'restart': {
      logger.info('SYSTEM', 'Restarting worker');
      // Capture the old worker's pid BEFORE shutdown so we can later prove
      // the worker answering health checks is a NEW process, not the corpse.
      const oldPid = await getCurrentWorkerPid(port, 2000);
      // Track whether the worker accepted the shutdown POST: an accepted
      // reason=restart shutdown means the dying worker spawns its OWN
      // successor the moment its port frees (worker-shutdown.ts handoff).
      // That handoff is the PRIMARY restart path; this CLI defers to it.
      const shutdownAccepted = await httpShutdown(port, 'restart');

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Check who holds the port: `lsof -i :<port>` (macOS/Linux) or `netstat -ano | findstr :<port>` (Windows)
  2. Kill the surviving worker process (its pid is in the claude-mem PID file) if it refuses to die
  3. Retry `claude-mem stop` — the second attempt usually succeeds once in-flight work drains
Defensive patterns

Strategy: retry

Validate before calling

async function assertPortFree(port: number): Promise<void> {
  const inUse = await isPortInUse(port);
  if (inUse) throw new Error(`port ${port} still bound after stop — inspect the owner before restarting`);
}

Prevention

When it happens

Trigger: The worker process ignores or delays shutdown (stuck in a long graceful task); a child process inherited the listening socket; lingering keep-alive HTTP connections keep it bound; another unrelated process grabbed the same port immediately after release.

Common situations: Stop issued while the observer is mid-summarization; older worker version with a slower shutdown path; container/WSL environments where socket teardown lags.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/e951faa5ec23c281. Report an issue: GitHub.