immich-app/immich · warning · BadRequestException

Job is already running

Error message

Job is already running

What it means

The private `start` handler for legacy job commands checks `jobRepository.isActive(name)` first and throws if the queue is already processing. It prevents enqueueing duplicate full-queue runs (e.g. a second VideoConversion pass while one runs). Legacy callers (runCommandLegacy) surface it directly.

Solutions

  1. Wait for the current job to finish before starting a new run
  2. Check queue status via the job/queue status endpoint first
  3. If a run is stuck, restart the worker/server to clear the active state, then retry
  4. Use the `force` option where supported to re-queue individual items instead of starting a duplicate queue run

Example fix

// before
await api.runJob('videoConversion'); // throws if already running
// after
const status = await api.getJobStatus('videoConversion');
if (!status.isActive) await api.runJob('videoConversion');
Defensive patterns

Strategy: validation

Validate before calling

const status = await api.getQueueStatus(name);
if (status.isActive) return { skipped: true };

Type guard

function canStart(q: { name: QueueName; isPaused: boolean; isActive?: boolean }): boolean {
  return !q.isActive;
}

Try / catch

try {
  await queueService.start(auth, name, { force });
} catch (e) {
  if (e instanceof BadRequestException && /already running/.test(e.message)) {
    return; // idempotent no-op
  }
  throw e;
}

Prevention

When it happens

Trigger: Issuing a start/run command for a queue (e.g. video conversion, storage template migration) while that same queue is currently active — e.g. clicking 'run' twice, or a cron firing while a manual run is in progress.

Common situations: Admin double-clicks a job button; scheduled job overlaps a manual one; a previous run is stuck so `isActive` still reports true and subsequent runs are rejected.

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 immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/c97867a37edd172e. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/queue.service.ts:190

  async emptyQueue(auth: AuthDto, name: QueueName, dto: QueueDeleteDto) {
    await this.jobRepository.empty(name);
    if (dto.failed) {
      await this.jobRepository.clear(name, QueueCleanType.Failed);
    }
  }

  private async getByName(name: QueueName): Promise<QueueResponseDto> {
    const [statistics, isPaused] = await Promise.all([
      this.jobRepository.getJobCounts(name),
      this.jobRepository.isPaused(name),
    ]);
    return { name, isPaused, statistics };
  }

  private async start(name: QueueName, { force }: QueueCommandDto): Promise<void> {
    const isActive = await this.jobRepository.isActive(name);
    if (isActive) {
      throw new BadRequestException(`Job is already running`);
    }

    await this.eventRepository.emit('QueueStart', { name });

    switch (name) {
      case QueueName.VideoConversion: {
        return this.jobRepository.queue({ name: JobName.AssetEncodeVideoQueueAll, data: { force } });
      }

      case QueueName.StorageTemplateMigration: {
        return this.jobRepository.queue({ name: JobName.StorageTemplateMigration });
      }

      case QueueName.Migration: {
        return this.jobRepository.queue({ name: JobName.FileMigrationQueueAll });
      }

      case QueueName.SmartSearch: {

View on GitHub (pinned to e55ac299a4)