thedotmack/claude-mem · info

Server was already stopped when close was requested

Error message

Server was already stopped when close was requested

What it means

GracefulShutdown closes the HTTP server with server.close(cb). Node invokes the callback with ERR_SERVER_NOT_RUNNING when the handle was never listening or was already closed. Per issue #3380 this end state is treated as success: the warning is logged, the promise resolves, and the remaining teardown (session drain, MCP close, chroma stop, db close, supervisor stop) still runs — previously a reject here aborted all of it.

Source

Thrown at src/services/infrastructure/GracefulShutdown.ts:81

  if (process.platform === 'win32') {
    await new Promise(r => setTimeout(r, 500));
  }

  await new Promise<void>((resolve, reject) => {
    server.close(err => {
      if (!err) {
        resolve();
        return;
      }
      // #3380 — Node's http.Server.close(cb) reports ERR_SERVER_NOT_RUNNING
      // when the handle is not listening (e.g. the bind failed or the server
      // already closed). Closing an already-closed server is the desired end
      // state, not a failure: rejecting here aborted ALL remaining teardown
      // (session drain, MCP close, chroma stop, db close, supervisor stop).
      // Same tolerance as ServerService.stop() in
      // src/server/runtime/ServerService.ts.
      if ((err as NodeJS.ErrnoException).code === 'ERR_SERVER_NOT_RUNNING') {
        logger.warn('SYSTEM', 'Server was already stopped when close was requested', {}, err);
        resolve();
        return;
      }
      reject(err);
    });
  });

  if (process.platform === 'win32') {
    await new Promise(r => setTimeout(r, 500));
    logger.info('SYSTEM', 'Waited for Windows port cleanup');
  }
}

View on GitHub (pinned to e2d1df569a)

Solutions

  1. No action required — the message records an already-desired state and teardown continues.
  2. If it appears alongside a startup bind error, fix the port conflict or EACCES at listen() time; that is the real failure.
  3. Expect at most one occurrence per double-stop; repeated occurrences indicate a shutdown loop worth investigating.

Example fix

// before: any close error aborted all remaining teardown
server.close(err => { if (err) reject(err); });
// after: already-stopped is the desired end state, keep tearing down
server.close(err => {
  if ((err as NodeJS.ErrnoException)?.code === 'ERR_SERVER_NOT_RUNNING') resolve();
  else if (err) reject(err);
  else resolve();
});
Defensive patterns

Strategy: try-catch

Type guard

function isServerNotRunning(e: unknown): e is NodeJS.ErrnoException {
  return (
    typeof e === 'object' && e !== null &&
    (e as NodeJS.ErrnoException).code === 'ERR_SERVER_NOT_RUNNING'
  );
}

Try / catch

await new Promise<void>((resolve, reject) => {
  server.close(err => {
    if (!err || (err as NodeJS.ErrnoException).code === 'ERR_SERVER_NOT_RUNNING') resolve();
    else reject(err);
  });
});

Prevention

When it happens

Trigger: close() is called on a server whose listen() failed earlier, or on a server another shutdown path already closed: double shutdown, restart flows, or a race between the monitor stopping the listener and the global shutdown cascade reaching it.

Common situations: Restart sequences that stop the worker twice; port-bind failure at startup followed by shutdown; teardown ordering where the HTTP server closes before the global shutdown begins.

Related errors


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