thedotmack/claude-mem · error
ServerJobQueue is already started
Error message
ServerJobQueue ${this.name} is already started What it means
ServerJobQueue.start() wires up the BullMQ worker/processor, which can only be attached once per queue instance. Calling start() again on an instance whose `started` flag is already true throws this error to prevent a second worker from being created.
Solutions
- Call start() exactly once per ServerJobQueue instance; guard the call site with an initialization flag
- Check the queue's started state (or your own flag) before invoking start
- Create a fresh ServerJobQueue instance if a full re-init is required
Example fix
// before
queue.start(processor);
// after (hot-reload safe)
if (!queueStarted) {
queue.start(processor);
queueStarted = true;
} Defensive patterns
Strategy: try-catch
Try / catch
try {
queue.start(processor);
} catch (e) {
if (e instanceof Error && e.message.includes('is already started')) {
return; // idempotent: queue already running
}
throw e;
} Prevention
- Call start() only from one initialization path (single bootstrap module)
- Track started state in an app-level singleton flag
- Avoid calling start() inside hot-reloadable or retried code blocks
When it happens
Trigger: Calling start(processor) twice on the same ServerJobQueue instance, e.g. re-initializing after a hot reload, restart routine, or retry path without constructing a new queue.
Common situations: Dev-server hot module reload re-running startup code; a retry wrapper calling start again after a partial failure; duplicated initialization in both server bootstrap and a plugin hook.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- BullMQ re-enqueue failed (will reconcile on startup)
- chroma-mcp connection cancelled during shutdown
- cloud sync must be configured before queueDelete
- Corpus " " has no session — call prime first
- failed to write operator audit row
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/0ca67fb62119541c.
Report an issue: GitHub.
Appendix: source
Thrown at src/server/jobs/ServerJobQueue.ts:247
}
}
// Single source of truth for queue-side error accounting. worker errors and
// QueueEvents errors both increment counters.errored and notify listeners,
// so per-process metrics aren't asymmetric across the two sources.
private notifyQueueError(error: unknown, source: 'worker' | 'queue-events'): void {
this.counters.errored += 1;
logger.warn('QUEUE', `${this.name} ${source} error`, {
error: error instanceof Error ? error.message : String(error),
});
for (const l of this.listeners) {
try { l.onError?.(error); } catch { /* listener errors must not propagate */ }
}
}
start(processor: Processor<TPayload>): void {
if (this.started) {
throw new Error(`ServerJobQueue ${this.name} is already started`);
}
const workerOptions: WorkerOptions = {
connection: this.config.connection,
prefix: this.config.prefix,
autorun: false,
concurrency: this.concurrency,
lockDuration: this.lockDurationMs
};
const worker = this.workerFactory
? this.workerFactory(this.name, processor, workerOptions)
: new Worker<TPayload>(this.name, processor, workerOptions);
worker.on('error', (error: unknown) => this.notifyQueueError(error, 'worker'));
// BullMQ Worker exposes `active`, `completed`, `failed`, `progress`, and
// `stalled` events. We attach to all five because the runtime relies on
// them for observability (Phase 12).
if (typeof (worker as { on?: unknown }).on === 'function') {
const w = worker as Worker<TPayload>;
w.on('active', (job: Job<TPayload>) => {View on GitHub (pinned to d8bc9755e7)