How Immich's asset.service.ts Manages the Complete Asset Lifecycle: Upload, Processing, and Deletion

TL;DR: Immich splits asset lifecycle management across a client-side service in web/src/lib/services/asset.service.ts that handles UI actions and a server-side service in server/src/services/asset.service.ts that orchestrates metadata persistence, background job queuing, and asynchronous deletion pipelines.

Immich uses a dual-layer service architecture to manage photos and videos from upload through final deletion. The asset.service.ts files—one in the web client and one in the server—coordinate to handle file ingestion, background processing like face detection and thumbnail generation, and safe cleanup workflows. Understanding these services reveals how the application maintains data integrity while performing heavy media operations asynchronously.

Upload and Replace Operations

The asset lifecycle begins when users upload files or replace existing assets through the web interface.

Client-Side Upload Handling

In web/src/lib/services/asset.service.ts, the handleReplaceAsset function orchestrates the replacement workflow. It opens the native file picker, uploads the new file, then coordinates the transfer of metadata from the old asset to the new one:

// web/src/lib/services/asset.service.ts
export const handleReplaceAsset = async (oldAssetId: string) => {
  const [newAssetId] = await openFileUploadDialog({ multiple: false });
  await copyAsset({ assetCopyDto: { sourceId: oldAssetId, targetId: newAssetId } });
  await deleteAssets({ assetBulkDeleteDto: { ids: [oldAssetId], force: true } });
  eventManager.emit('AssetReplace', { oldAssetId, newAssetId });
};

This client-side asset.service.ts acts as a thin wrapper around the SDK, delegating the actual business logic to the server while managing UI state updates through events.

Server-Side Copy Logic

When the client calls copyAsset, the server-side asset.service.ts executes the copy method. This preserves album associations, sidecar metadata, and stack relationships from the source asset to the target. The server validates permissions via requireAccess before duplicating database records and queuing background jobs like AssetExtractMetadata for the new copy.

Background Processing Pipeline

Heavy media operations run asynchronously through a job queue system managed by the server-side asset.service.ts.

Job Queuing Architecture

The run method in server/src/services/asset.service.ts validates user permissions then dispatches specific jobs based on the requested action:

// server/src/services/asset.service.ts
async run(auth: AuthDto, dto: AssetJobsDto) {
  await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds });
  const jobs: JobItem[] = [];

  for (const id of dto.assetIds) {
    switch (dto.name) {
      case AssetJobName.REFRESH_FACES:
        jobs.push({ name: JobName.AssetDetectFaces, data: { id } });
        break;
      case AssetJobName.REGENERATE_THUMBNAIL:
        jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id } });
        break;
      // Additional job types...
    }
  }
  await this.jobRepository.queueAll(jobs);
}

The service supports multiple AssetJobName values including RefreshFaces, RegenerateThumbnail, TranscodeVideo, and ExtractMetadata, each mapping to specific queue workers.

Metadata Updates and Sidecar Writing

When users edit EXIF data, descriptions, or ratings, the server-side asset.service.ts handles these through the update and updateExif methods. These operations queue SidecarWrite jobs to persist changes back to XMP sidecar files, ensuring metadata remains synchronized between the database and filesystem.

Deletion and Cleanup Workflow

The asset.service.ts implements a two-phase deletion system that prevents accidental data loss while ensuring eventual cleanup of storage space.

Soft Delete and Trashing

The deleteAll method in server/src/services/asset.service.ts handles both trashing and permanent deletion requests:

// server/src/services/asset.service.ts
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto) {
  const { ids, force } = dto;
  await this.requireAccess({ auth, permission: Permission.AssetDelete, ids });
  await this.assetRepository.updateAll(ids, {
    deletedAt: new Date(),
    status: force ? AssetStatus.Deleted : AssetStatus.Trashed,
  });
  await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', {
    assetIds: ids,
    userId: auth.user.id,
  });
}

When force is false, assets enter the Trashed state and remain recoverable until the retention period expires.

Periodic Deletion Check

A scheduled background job AssetDeleteCheck triggers the permanent cleanup process. The handleAssetDeletionCheck method in server/src/services/asset.service.ts streams assets past their trash retention window and batches them for deletion:

// server/src/services/asset.service.ts
@OnJob({ name: JobName.AssetDeleteCheck, queue: QueueName.BackgroundTask })
async handleAssetDeletionCheck(): Promise<JobStatus> {
  const config = await this.getConfig({ withCache: false });
  const trashedDays = config.trash.enabled ? config.trash.days : 0;
  const trashedBefore = DateTime.now().minus(Duration.fromObject({ days: trashedDays })).toJSDate();

  const assets = this.assetJobRepository.streamForDeletedJob(trashedBefore);
  for await (const asset of assets) {
    chunk.push(asset);
    if (chunk.length >= JOBS_ASSET_PAGINATION_SIZE) await queueChunk();
  }
  await queueChunk();
  return JobStatus.Success;
}

Physical File Removal

The final AssetDelete job handles complex cleanup logic including stack management, quota updates, and motion-photo reference counting. The handleAssetDeletion method builds a comprehensive list of physical files to remove:

// server/src/services/asset.service.ts
@OnJob({ name: JobName.AssetDelete, queue: QueueName.BackgroundTask })
async handleAssetDeletion(job: JobOf<JobName.AssetDelete>): Promise<JobStatus> {
  const { id, deleteOnDisk } = job;
  const asset = await this.assetJobRepository.getForAssetDeletion(id);
  
  // Stack primary replacement logic...
  // Database removal and quota update...
  
  const files = [
    assetFiles.thumbnailFile?.path,
    assetFiles.previewFile?.path,
    asset.originalPath,
    // ... additional file paths
  ];
  
  if (deleteOnDisk && !asset.isOffline) {
    await this.jobRepository.queue({ 
      name: JobName.FileDelete, 
      data: { files: files.filter(Boolean) } 
    });
  }
  return JobStatus.Success;
}

This queues a FileDelete job that actually removes files from disk or object storage, completing the asset lifecycle.

Practical Implementation Examples

Replacing an Asset via Client Service

import { handleReplaceAsset } from '$lib/services/asset.service';

// Initiates replacement workflow: upload → copy metadata → delete old
await handleReplaceAsset('old-asset-uuid');

Triggering Background Jobs

import { runAssetJobs, AssetJobName } from '@immich/sdk';

// Queue face detection for specific assets
await runAssetJobs({ 
  assetJobsDto: { 
    name: AssetJobName.RefreshFaces, 
    assetIds: ['asset-id-1', 'asset-id-2'] 
  } 
});

Manual Deletion Check

Administrators can manually trigger the cleanup cycle:

import { queue } from '@immich/sdk';

// Immediately enqueue trash cleanup regardless of schedule
await queue({ name: 'AssetDeleteCheck' });

Summary

  • Dual-layer architecture: The web client's asset.service.ts manages UI state and SDK calls, while the server's asset.service.ts implements core business logic and job orchestration.
  • Asynchronous processing: All heavy operations (face detection, thumbnail generation, transcoding) queue through the server's run method and process via background workers.
  • Safe deletion workflow: Assets transition through trashed states with configurable retention, followed by automated AssetDeleteCheck jobs that handle stack management, quota updates, and physical file cleanup.
  • Permission enforcement: Every operation validates access through requireAccess before modifying database state or queuing jobs.

Frequently Asked Questions

What is the difference between the client and server asset.service.ts files?

The client-side web/src/lib/services/asset.service.ts provides UI-focused helpers like handleReplaceAsset and handleRunAssetJob that wrap SDK calls and manage local state. The server-side server/src/services/asset.service.ts contains the authoritative business logic, database transactions, permission checks, and job queuing that ensure data consistency across the Immich cluster.

How does Immich handle asset replacement without losing metadata?

When replacing an asset, the client service calls copyAsset to duplicate the old asset's album associations, sidecar files, and stack relationships onto the newly uploaded file. After the copy succeeds, it force-deletes the original asset ID. This server-side copy operation preserves all metadata while pointing references to the new file hash.

What happens to physical files when an asset is deleted?

Deletion occurs in stages. First, deleteAll marks assets as trashed in the database. After the retention period expires, handleAssetDeletionCheck queues individual AssetDelete jobs. These jobs update database records, adjust user storage quotas, and finally queue FileDelete jobs that remove thumbnails, previews, and original files from disk or S3 storage only if the asset is not marked offline.

Can administrators manually trigger background asset processing?

Yes. While the web interface provides buttons for actions like "Refresh Faces" or "Regenerate Thumbnails" that call the client service, administrators can also manually enqueue jobs directly using the SDK's queue method with job names like AssetDeleteCheck, AssetDetectFaces, or AssetGenerateThumbnails to force immediate processing outside the normal schedule.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →