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
- 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
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
- 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
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
- Task with id not found
- Invalid job name
- The BackgroundTask queue cannot be paused
- Unable to set queue concurrency, worker not found
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)