diff --git a/e2e/src/api/specs/jobs.e2e-spec.ts b/e2e/src/api/specs/jobs.e2e-spec.ts
new file mode 100644
index 0000000000..4b8126c941
--- /dev/null
+++ b/e2e/src/api/specs/jobs.e2e-spec.ts
@@ -0,0 +1,86 @@
+import { JobCommand, JobName, LoginResponseDto } from '@immich/sdk';
+import { readFile } from 'node:fs/promises';
+import { basename } from 'node:path';
+import { errorDto } from 'src/responses';
+import { app, testAssetDir, utils } from 'src/utils';
+import request from 'supertest';
+import { afterEach, beforeAll, describe, expect, it } from 'vitest';
+
+describe('/jobs', () => {
+  let admin: LoginResponseDto;
+
+  beforeAll(async () => {
+    await utils.resetDatabase();
+    admin = await utils.adminSetup({ onboarding: false });
+  });
+
+  describe('PUT /jobs', () => {
+    afterEach(async () => {
+      await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
+        command: JobCommand.Resume,
+        force: false,
+      });
+    });
+
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).put('/jobs/metadataExtraction');
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should queue metadata extraction for missing assets', async () => {
+      const path1 = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
+      const path2 = `${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`;
+
+      await utils.createAsset(admin.accessToken, {
+        assetData: { bytes: await readFile(path1), filename: basename(path1) },
+      });
+
+      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+      await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
+        command: JobCommand.Pause,
+        force: false,
+      });
+
+      const { id } = await utils.createAsset(admin.accessToken, {
+        assetData: { bytes: await readFile(path2), filename: basename(path2) },
+      });
+
+      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+      {
+        const asset = await utils.getAssetInfo(admin.accessToken, id);
+
+        expect(asset.exifInfo).toBeDefined();
+        expect(asset.exifInfo?.make).toBeNull();
+      }
+
+      await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
+        command: JobCommand.Empty,
+        force: false,
+      });
+
+      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+      await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
+        command: JobCommand.Resume,
+        force: false,
+      });
+
+      await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
+        command: JobCommand.Start,
+        force: false,
+      });
+
+      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+
+      {
+        const asset = await utils.getAssetInfo(admin.accessToken, id);
+
+        expect(asset.exifInfo).toBeDefined();
+        expect(asset.exifInfo?.make).toBe('NIKON CORPORATION');
+      }
+    });
+  });
+});
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index efd9ce76b9..d303b71eaa 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -6,6 +6,8 @@ import {
   CheckExistingAssetsDto,
   CreateAlbumDto,
   CreateLibraryDto,
+  JobCommandDto,
+  JobName,
   MetadataSearchDto,
   Permission,
   PersonCreateDto,
@@ -29,6 +31,7 @@ import {
   getConfigDefaults,
   login,
   searchAssets,
+  sendJobCommand,
   setBaseUrl,
   signUpAdmin,
   tagAssets,
@@ -475,6 +478,9 @@ export const utils = {
   tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
     tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
 
+  jobCommand: async (accessToken: string, jobName: JobName, jobCommandDto: JobCommandDto) =>
+    sendJobCommand({ id: jobName, jobCommandDto }, { headers: asBearerAuth(accessToken) }),
+
   setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
     await context.addCookies([
       {
diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts
index 1f52b9c71a..201f295d49 100644
--- a/server/src/repositories/asset.repository.ts
+++ b/server/src/repositories/asset.repository.ts
@@ -467,8 +467,8 @@ export class AssetRepository implements IAssetRepository {
       )
       .$if(property === WithoutProperty.EXIF, (qb) =>
         qb
-          .innerJoin('asset_job_status as job_status', 'assets.id', 'job_status.assetId')
-          .where('job_status.metadataExtractedAt', 'is', null)
+          .leftJoin('asset_job_status as job_status', 'assets.id', 'job_status.assetId')
+          .where((eb) => eb.or([eb('job_status.metadataExtractedAt', 'is', null), eb('assetId', 'is', null)]))
           .where('assets.isVisible', '=', true),
       )
       .$if(property === WithoutProperty.FACES, (qb) =>