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
- Check who holds the port: `lsof -i :<port>` (macOS/Linux) or `netstat -ano | findstr :<port>` (Windows)
- Kill the surviving worker process (its pid is in the claude-mem PID file) if it refuses to die
- 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
- After stop, verify the port is free before issuing start/restart
- Avoid issuing concurrent stop/restart commands to the same worker
- If stop repeatedly fails, kill the pid from the PID file rather than spawning around a stuck listener
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
- Port still bound entering restart fallback — verifying curre
- `server ${commandLabel}` is a server runtime command, but CL
- [uninstall] Worker shutdown attempt failed:
- Shutdown request returned error
- Shutdown already in progress — ignoring re-entrant shutdown
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/e951faa5ec23c281.
Report an issue: GitHub.