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

> Explore how Immichs asset.service.ts manages the complete asset lifecycle from upload and processing to deletion. Understand client and server-side responsibilities for efficient asset management.

- Repository: [Immich/immich](https://github.com/immich-app/immich)
- Tags: internals
- Published: 2026-02-27

---

**TL;DR:** Immich splits asset lifecycle management across a client-side service in [`web/src/lib/services/asset.service.ts`](https://github.com/immich-app/immich/blob/main/web/src/lib/services/asset.service.ts) that handles UI actions and a server-side service in [`server/src/services/asset.service.ts`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/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:

```typescript
// 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`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/asset.service.ts).

### Job Queuing Architecture

The `run` method in [`server/src/services/asset.service.ts`](https://github.com/immich-app/immich/blob/main/server/src/services/asset.service.ts) validates user permissions then dispatches specific jobs based on the requested action:

```typescript
// 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`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/server/src/services/asset.service.ts) handles both trashing and permanent deletion requests:

```typescript
// 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`](https://github.com/immich-app/immich/blob/main/server/src/services/asset.service.ts) streams assets past their trash retention window and batches them for deletion:

```typescript
// 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:

```typescript
// 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

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

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

```

### Triggering Background Jobs

```typescript
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:

```typescript
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`](https://github.com/immich-app/immich/blob/main/asset.service.ts) manages UI state and SDK calls, while the server's [`asset.service.ts`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/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.