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

  1. Call start() exactly once per ServerJobQueue instance; guard the call site with an initialization flag
  2. Check the queue's started state (or your own flag) before invoking start
  3. 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

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


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)