thedotmack/claude-mem · warning
Port did not free up after shutdown
Error message
Port did not free up after shutdown
What it means
During worker shutdown (stop flow), after httpShutdown the code waits up to the platform-adjusted 15s for the port to become free. If the port is still occupied it logs this warning but continues: it removes the PID file only if it belongs to the just-stopped PID and exits 0, leaving the port possibly still held.
Solutions
- Find and kill the lingering process: lsof -i :<port> then kill -9 the PID.
- Wait a few seconds and retry the stop/start — the port usually frees once the process exits.
- Use the platform timeout env/tuning if 15s is too short for slow shutdowns on your platform.
- Check for orphaned child processes of the worker (chroma/uv) that keep the socket alive and terminate them.
Example fix
// before: blind restart claude-mem stop && claude-mem start // after: ensure the port is free first claude-mem stop lsof -ti :37777 | xargs -r kill -9 claude-mem start
Defensive patterns
Strategy: retry
Validate before calling
async function isPortFree(port) {
return new Promise(res => {
const s = require('net').createServer();
s.once('error', () => res(false));
s.once('listening', () => s.close(() => res(true)));
s.listen(port);
});
} Try / catch
const freed = await waitForPortFree(port, 15000);
if (!freed) {
const pids = execSync(`lsof -ti :${port}`).toString().trim();
if (pids) process.kill(Number(pids.split('\n')[0]), 'SIGKILL');
} Prevention
- Ensure the worker handles SIGTERM promptly and exits its background threads
- Watch for orphaned children (chroma/uv) after shutdown
- Insert a small delay between stop and start when scripting restarts
When it happens
Trigger: The stop command's waitForPortFree times out because the worker process (or a child) ignores SIGTERM and lingers, another process grabbed the port, or the socket sits in a lingering state.
Common situations: Unresponsive worker due to a hung request or stuck background thread; orphaned child process holding the listening socket; restarting too quickly so TIME_WAIT/old process still owns the port.
Related errors
- Worker shutdown attempt failed
- Worker shutdown failed
- chroma-mcp call cancelled during shutdown
- failed to kill in-flight chroma-mcp prewarm tree…
- failed to kill prior chroma-mcp tree (best-effort)
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/e951faa5ec23c281.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/worker-service.ts:1194
exitWithStatus(
'error',
bootFailure ? `Failed to start worker: ${bootFailure}` : '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 d8bc9755e7)