{"record":{"id":"c97867a37edd172e","repo":"immich-app/immich","slug":"job-is-already-running","errorCode":null,"errorMessage":"Job is already running","messagePattern":"Job is already running","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/queue.service.ts","lineNumber":190,"sourceCode":"  async emptyQueue(auth: AuthDto, name: QueueName, dto: QueueDeleteDto) {\n    await this.jobRepository.empty(name);\n    if (dto.failed) {\n      await this.jobRepository.clear(name, QueueCleanType.Failed);\n    }\n  }\n\n  private async getByName(name: QueueName): Promise<QueueResponseDto> {\n    const [statistics, isPaused] = await Promise.all([\n      this.jobRepository.getJobCounts(name),\n      this.jobRepository.isPaused(name),\n    ]);\n    return { name, isPaused, statistics };\n  }\n\n  private async start(name: QueueName, { force }: QueueCommandDto): Promise<void> {\n    const isActive = await this.jobRepository.isActive(name);\n    if (isActive) {\n      throw new BadRequestException(`Job is already running`);\n    }\n\n    await this.eventRepository.emit('QueueStart', { name });\n\n    switch (name) {\n      case QueueName.VideoConversion: {\n        return this.jobRepository.queue({ name: JobName.AssetEncodeVideoQueueAll, data: { force } });\n      }\n\n      case QueueName.StorageTemplateMigration: {\n        return this.jobRepository.queue({ name: JobName.StorageTemplateMigration });\n      }\n\n      case QueueName.Migration: {\n        return this.jobRepository.queue({ name: JobName.FileMigrationQueueAll });\n      }\n\n      case QueueName.SmartSearch: {","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/services/queue.service.ts#L172-L208","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Wait for the current job to finish before starting a new run","Check queue status via the job/queue status endpoint first","If a run is stuck, restart the worker/server to clear the active state, then retry","Use the `force` option where supported to re-queue individual items instead of starting a duplicate queue run"],"exampleFix":"// before\nawait api.runJob('videoConversion'); // throws if already running\n// after\nconst status = await api.getJobStatus('videoConversion');\nif (!status.isActive) await api.runJob('videoConversion');","handlingStrategy":"validation","validationCode":"const status = await api.getQueueStatus(name);\nif (status.isActive) return { skipped: true };","typeGuard":"function canStart(q: { name: QueueName; isPaused: boolean; isActive?: boolean }): boolean {\n  return !q.isActive;\n}","tryCatchPattern":"try {\n  await queueService.start(auth, name, { force });\n} catch (e) {\n  if (e instanceof BadRequestException && /already running/.test(e.message)) {\n    return; // idempotent no-op\n  }\n  throw e;\n}","preventionTips":["Check queue status before issuing run commands","Debounce/deduplicate job buttons in UIs","Add locking around cron-triggered runs","Handle stuck runs with worker restarts"],"tags":["queue","concurrency","duplicate-run"],"backgroundTag":"invalid-state-transition","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}