thedotmack/claude-mem · warning

Graceful shutdown deadline exceeded — proceeding

Error message

Graceful shutdown deadline exceeded — proceeding

What it means

runShutdownSequence races graceful cleanup against a deadline. When the graceful branch loses the race, the sequence logs that the deadline was exceeded and proceeds (exits) anyway rather than hanging forever. In-flight work may be truncated.

Solutions

  1. Increase gracefulDeadlineMs in shutdown options to cover worst-case flush time
  2. Check what the graceful branch was doing (queue depth, SDK logs) when it timed out
  3. Reduce pending work at shutdown time (flush more frequently during operation)
  4. Ensure the SDK subprocess is killed promptly so it cannot block the race

Example fix

// before
runShutdownSequence({ gracefulDeadlineMs: 3000, ... })
// after
runShutdownSequence({ gracefulDeadlineMs: 10000, ... })
Defensive patterns

Strategy: try-catch

Validate before calling

// Estimate flush time before entering shutdown
const pending = getPendingObservationCount();
const safeDeadline = Math.max(gracefulDeadlineMs, pending * PER_ITEM_MS + 2000);

Try / catch

try {
  await runShutdownSequence({ gracefulDeadlineMs: 10000, ... });
} finally {
  // exit anyway; deadline already enforced internally
}

Prevention

When it happens

Trigger: The graceful shutdown work (flushing observations, SDK queries, HTTP shutdown POST) did not complete before options.gracefulDeadlineMs elapsed, so Promise.race resolved with 'deadline' and the warn fires.

Common situations: Slow disk or DB making observation flush exceed the deadline; an SDK query hanging during shutdown; deadline configured too aggressively for the machine; large queue of pending memory writes at exit.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/64322dd5d4dc1da6. Report an issue: GitHub.

Appendix: source

Thrown at src/services/worker-shutdown.ts:115

    const outcome = await Promise.race([
      options.performGracefulShutdown().then(
        () => 'graceful' as const,
        (error: unknown) => {
          // A failed graceful shutdown must not abort the restart handoff;
          // proceed exactly like the deadline path.
          logger.error(
            'SYSTEM',
            'Graceful shutdown failed — proceeding',
            { reason: options.reason },
            error instanceof Error ? error : new Error(String(error))
          );
          return 'graceful-error' as const;
        }
      ),
      deadline,
    ]);
    if (outcome === 'deadline') {
      logger.warn('SYSTEM', 'Graceful shutdown deadline exceeded — proceeding', {
        deadlineMs: options.gracefulDeadlineMs,
        reason: options.reason,
      });
    }
  } finally {
    if (deadlineTimer !== undefined) clearTimeout(deadlineTimer);
  }

  // Successor handoff — ONLY for restart; 'stop' and signal shutdowns stay
  // kill-only. The old worker spawns its replacement as its final act, after
  // its port is confirmed free, so the successor never races the corpse for
  // the port. CLI `claude-mem restart` is the caller. Hook version-mismatch
  // recycles (ensureWorkerRunning in src/shared/worker-utils.ts) never reach
  // this: they SIGKILL the stale worker and lazy-spawn the resolved version
  // themselves, because this handoff runs the DYING install's resolver — a
  // stale install would respawn its own version forever (#3378). This runs
  // inside flushResponseThen's flushed action, so it completes before that
  // helper's process.exit(0).

View on GitHub (pinned to d8bc9755e7)