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
- No action required — the message records an already-desired state and teardown continues.
- If it appears alongside a startup bind error, fix the port conflict or EACCES at listen() time; that is the real failure.
- 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
- Treat close() on an already-closed server as success in every shutdown path.
- Keep shutdown idempotent: one supervisor owns the teardown sequence.
- Test restart flows for double-stop races.
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
- [uninstall] Worker shutdown attempt failed:
- failed to kill prior chroma-mcp tree (best-effort)
- failed to kill in-flight chroma-mcp prewarm tree (best-effor
- ${ctx.component} failed during ${ctx.phase}: ${causeMessage(
- Failed to install Bun. Please install manually: ${manualInst
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/25ccc518453c85d2.
Report an issue: GitHub.