diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart
index e1f6b44e5b..50ee28f0af 100644
--- a/mobile/openapi/lib/model/person_response_dto.dart
+++ b/mobile/openapi/lib/model/person_response_dto.dart
@@ -18,6 +18,7 @@ class PersonResponseDto {
     required this.isHidden,
     required this.name,
     required this.thumbnailPath,
+    this.updatedAt,
   });
 
   DateTime? birthDate;
@@ -30,13 +31,23 @@ class PersonResponseDto {
 
   String thumbnailPath;
 
+  /// This property was added in v1.107.0
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  DateTime? updatedAt;
+
   @override
   bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
     other.birthDate == birthDate &&
     other.id == id &&
     other.isHidden == isHidden &&
     other.name == name &&
-    other.thumbnailPath == thumbnailPath;
+    other.thumbnailPath == thumbnailPath &&
+    other.updatedAt == updatedAt;
 
   @override
   int get hashCode =>
@@ -45,10 +56,11 @@ class PersonResponseDto {
     (id.hashCode) +
     (isHidden.hashCode) +
     (name.hashCode) +
-    (thumbnailPath.hashCode);
+    (thumbnailPath.hashCode) +
+    (updatedAt == null ? 0 : updatedAt!.hashCode);
 
   @override
-  String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]';
+  String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -61,6 +73,11 @@ class PersonResponseDto {
       json[r'isHidden'] = this.isHidden;
       json[r'name'] = this.name;
       json[r'thumbnailPath'] = this.thumbnailPath;
+    if (this.updatedAt != null) {
+      json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String();
+    } else {
+    //  json[r'updatedAt'] = null;
+    }
     return json;
   }
 
@@ -77,6 +94,7 @@ class PersonResponseDto {
         isHidden: mapValueOfType<bool>(json, r'isHidden')!,
         name: mapValueOfType<String>(json, r'name')!,
         thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
+        updatedAt: mapDateTime(json, r'updatedAt', r''),
       );
     }
     return null;
diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart
index b15e620250..af2e7101c3 100644
--- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart
+++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart
@@ -19,6 +19,7 @@ class PersonWithFacesResponseDto {
     required this.isHidden,
     required this.name,
     required this.thumbnailPath,
+    this.updatedAt,
   });
 
   DateTime? birthDate;
@@ -33,6 +34,15 @@ class PersonWithFacesResponseDto {
 
   String thumbnailPath;
 
+  /// This property was added in v1.107.0
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  DateTime? updatedAt;
+
   @override
   bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
     other.birthDate == birthDate &&
@@ -40,7 +50,8 @@ class PersonWithFacesResponseDto {
     other.id == id &&
     other.isHidden == isHidden &&
     other.name == name &&
-    other.thumbnailPath == thumbnailPath;
+    other.thumbnailPath == thumbnailPath &&
+    other.updatedAt == updatedAt;
 
   @override
   int get hashCode =>
@@ -50,10 +61,11 @@ class PersonWithFacesResponseDto {
     (id.hashCode) +
     (isHidden.hashCode) +
     (name.hashCode) +
-    (thumbnailPath.hashCode);
+    (thumbnailPath.hashCode) +
+    (updatedAt == null ? 0 : updatedAt!.hashCode);
 
   @override
-  String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]';
+  String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -67,6 +79,11 @@ class PersonWithFacesResponseDto {
       json[r'isHidden'] = this.isHidden;
       json[r'name'] = this.name;
       json[r'thumbnailPath'] = this.thumbnailPath;
+    if (this.updatedAt != null) {
+      json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String();
+    } else {
+    //  json[r'updatedAt'] = null;
+    }
     return json;
   }
 
@@ -84,6 +101,7 @@ class PersonWithFacesResponseDto {
         isHidden: mapValueOfType<bool>(json, r'isHidden')!,
         name: mapValueOfType<String>(json, r'name')!,
         thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
+        updatedAt: mapDateTime(json, r'updatedAt', r''),
       );
     }
     return null;
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index d403cf7530..0ac2cd53c9 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -9345,6 +9345,11 @@
           },
           "thumbnailPath": {
             "type": "string"
+          },
+          "updatedAt": {
+            "description": "This property was added in v1.107.0",
+            "format": "date-time",
+            "type": "string"
           }
         },
         "required": [
@@ -9414,6 +9419,11 @@
           },
           "thumbnailPath": {
             "type": "string"
+          },
+          "updatedAt": {
+            "description": "This property was added in v1.107.0",
+            "format": "date-time",
+            "type": "string"
           }
         },
         "required": [
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 7a1da9d13d..ddf6c958b8 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -158,6 +158,8 @@ export type PersonWithFacesResponseDto = {
     isHidden: boolean;
     name: string;
     thumbnailPath: string;
+    /** This property was added in v1.107.0 */
+    updatedAt?: string;
 };
 export type SmartInfoResponseDto = {
     objects?: string[] | null;
@@ -432,6 +434,8 @@ export type PersonResponseDto = {
     isHidden: boolean;
     name: string;
     thumbnailPath: string;
+    /** This property was added in v1.107.0 */
+    updatedAt?: string;
 };
 export type AssetFaceResponseDto = {
     boundingBoxX1: number;
diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts
index b28f18603a..3ad41ecff2 100644
--- a/server/src/dtos/person.dto.ts
+++ b/server/src/dtos/person.dto.ts
@@ -1,6 +1,7 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { Type } from 'class-transformer';
 import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator';
+import { PropertyLifecycle } from 'src/decorators';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { PersonEntity } from 'src/entities/person.entity';
@@ -71,6 +72,8 @@ export class PersonResponseDto {
   birthDate!: Date | null;
   thumbnailPath!: string;
   isHidden!: boolean;
+  @PropertyLifecycle({ addedAt: 'v1.107.0' })
+  updatedAt?: Date;
 }
 
 export class PersonWithFacesResponseDto extends PersonResponseDto {
@@ -138,6 +141,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
     birthDate: person.birthDate,
     thumbnailPath: person.thumbnailPath,
     isHidden: person.isHidden,
+    updatedAt: person.updatedAt,
   };
 }
 
diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts
index eb0e3ad1e9..2aea5c2798 100644
--- a/server/src/services/person.service.spec.ts
+++ b/server/src/services/person.service.spec.ts
@@ -42,6 +42,7 @@ const responseDto: PersonResponseDto = {
   birthDate: null,
   thumbnailPath: '/path/to/thumbnail.jpg',
   isHidden: false,
+  updatedAt: expect.any(Date),
 };
 
 const statistics = { assets: 3 };
@@ -126,6 +127,7 @@ describe(PersonService.name, () => {
             birthDate: null,
             thumbnailPath: '/path/to/thumbnail.jpg',
             isHidden: true,
+            updatedAt: expect.any(Date),
           },
         ],
       });
@@ -255,6 +257,7 @@ describe(PersonService.name, () => {
         birthDate: new Date('1976-06-30'),
         thumbnailPath: '/path/to/thumbnail.jpg',
         isHidden: false,
+        updatedAt: expect.any(Date),
       });
       expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
       expect(jobMock.queue).not.toHaveBeenCalled();
@@ -407,6 +410,7 @@ describe(PersonService.name, () => {
         id: personStub.noName.id,
         name: personStub.noName.name,
         thumbnailPath: personStub.noName.thumbnailPath,
+        updatedAt: expect.any(Date),
       });
 
       expect(jobMock.queue).not.toHaveBeenCalledWith();
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index ac3a4b4c9e..6af265e3da 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -213,7 +213,7 @@
                 <ImageThumbnail
                   curve
                   shadow
-                  url={getPeopleThumbnailUrl(person.id)}
+                  url={getPeopleThumbnailUrl(person)}
                   altText={person.name}
                   title={person.name}
                   widthStyle="90px"
diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte
index 975dad5b95..1b6894dd91 100644
--- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte
+++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte
@@ -108,7 +108,7 @@
                 <ImageThumbnail
                   curve
                   shadow
-                  url={getPeopleThumbnailUrl(person.id)}
+                  url={getPeopleThumbnailUrl(person)}
                   altText={getPersonNameWithHiddenValue(person.name, person.isHidden)}
                   title={getPersonNameWithHiddenValue(person.name, person.isHidden)}
                   widthStyle="90px"
diff --git a/web/src/lib/components/faces-page/face-thumbnail.svelte b/web/src/lib/components/faces-page/face-thumbnail.svelte
index bb17ca5795..58e1e0e39b 100644
--- a/web/src/lib/components/faces-page/face-thumbnail.svelte
+++ b/web/src/lib/components/faces-page/face-thumbnail.svelte
@@ -36,7 +36,7 @@
     class:dark:border-immich-dark-primary={border}
     class:border-immich-primary={border}
   >
-    <ImageThumbnail {circle} url={getPeopleThumbnailUrl(person.id)} altText={person.name} widthStyle="100%" shadow />
+    <ImageThumbnail {circle} url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" shadow />
   </div>
 
   <div
diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
index 3f8a8b36d2..d781e1cc56 100644
--- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
+++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
@@ -38,7 +38,7 @@
         <ImageThumbnail
           circle
           shadow
-          url={getPeopleThumbnailUrl(personMerge1.id)}
+          url={getPeopleThumbnailUrl(personMerge1)}
           altText={personMerge1.name}
           widthStyle="100%"
         />
@@ -65,7 +65,7 @@
           border={potentialMergePeople.length > 0}
           circle
           shadow
-          url={getPeopleThumbnailUrl(personMerge2.id)}
+          url={getPeopleThumbnailUrl(personMerge2)}
           altText={personMerge2.name}
           widthStyle="100%"
         />
@@ -84,7 +84,7 @@
                     border={true}
                     circle
                     shadow
-                    url={getPeopleThumbnailUrl(person.id)}
+                    url={getPeopleThumbnailUrl(person)}
                     altText={person.name}
                     widthStyle="100%"
                     on:click={() => changePersonToMerge(person)}
diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte
index b532899353..b5a00cf23d 100644
--- a/web/src/lib/components/faces-page/people-card.svelte
+++ b/web/src/lib/components/faces-page/people-card.svelte
@@ -50,7 +50,7 @@
       <ImageThumbnail
         shadow
         {preload}
-        url={getPeopleThumbnailUrl(person.id)}
+        url={getPeopleThumbnailUrl(person)}
         altText={person.name}
         title={person.name}
         widthStyle="100%"
diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte
index a848a17e75..103f3f9a6b 100644
--- a/web/src/lib/components/faces-page/person-side-panel.svelte
+++ b/web/src/lib/components/faces-page/person-side-panel.svelte
@@ -234,7 +234,7 @@
                   <ImageThumbnail
                     curve
                     shadow
-                    url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id].id)}
+                    url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id])}
                     altText={selectedPersonToReassign[face.id].name}
                     title={getPersonNameWithHiddenValue(
                       selectedPersonToReassign[face.id].name,
@@ -248,7 +248,7 @@
                   <ImageThumbnail
                     curve
                     shadow
-                    url={getPeopleThumbnailUrl(face.person.id)}
+                    url={getPeopleThumbnailUrl(face.person)}
                     altText={face.person.name}
                     title={getPersonNameWithHiddenValue(face.person.name, face.person.isHidden)}
                     widthStyle={thumbnailWidth}
diff --git a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte
index 0ede8d7fa1..9eec526f4a 100644
--- a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte
@@ -71,13 +71,7 @@
               : 'border-transparent'}"
             on:click={() => togglePersonSelection(person.id)}
           >
-            <ImageThumbnail
-              circle
-              shadow
-              url={getPeopleThumbnailUrl(person.id)}
-              altText={person.name}
-              widthStyle="100%"
-            />
+            <ImageThumbnail circle shadow url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" />
             <p class="mt-2 line-clamp-2 text-sm font-medium dark:text-white">{person.name}</p>
           </button>
         {/each}
diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json
index c8e9ac49b5..9e6eb74894 100644
--- a/web/src/lib/i18n/en.json
+++ b/web/src/lib/i18n/en.json
@@ -621,6 +621,7 @@
     "unable_to_save_settings": "Unable to save settings",
     "unable_to_scan_libraries": "Unable to scan libraries",
     "unable_to_scan_library": "Unable to scan library",
+    "unable_to_set_feature_photo": "Unable to set feature photo",
     "unable_to_set_profile_picture": "Unable to set profile picture",
     "unable_to_submit_job": "Unable to submit job",
     "unable_to_trash_asset": "Unable to trash asset",
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index 7743d9b592..58bf49c43b 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -17,6 +17,7 @@ import {
   startOAuth,
   unlinkOAuthAccount,
   type AssetResponseDto,
+  type PersonResponseDto,
   type SharedLinkResponseDto,
 } from '@immich/sdk';
 import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js';
@@ -205,7 +206,8 @@ export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: s
 
 export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileImagePath(userId));
 
-export const getPeopleThumbnailUrl = (personId: string) => createUrl(getPeopleThumbnailPath(personId));
+export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) =>
+  createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt });
 
 export const getAssetJobName = derived(t, ($t) => {
   return (job: AssetJobName) => {
diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte
index 3b25cdc0c1..d0d7a9936f 100644
--- a/web/src/routes/(user)/explore/+page.svelte
+++ b/web/src/routes/(user)/explore/+page.svelte
@@ -61,7 +61,7 @@
               <ImageThumbnail
                 circle
                 shadow
-                url={getPeopleThumbnailUrl(person.id)}
+                url={getPeopleThumbnailUrl(person)}
                 altText={person.name}
                 widthStyle="100%"
               />
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte
index 432410798f..41db2c05b6 100644
--- a/web/src/routes/(user)/people/+page.svelte
+++ b/web/src/routes/(user)/people/+page.svelte
@@ -508,7 +508,7 @@
             preload={searchName !== '' || index < 20}
             bind:hidden={person.isHidden}
             shadow
-            url={getPeopleThumbnailUrl(person.id)}
+            url={getPeopleThumbnailUrl(person)}
             altText={person.name}
             widthStyle="100%"
             bind:eyeColor={eyeColorMap[person.id]}
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index eecfbf29b2..f1fbe716a6 100644
--- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -91,7 +91,7 @@
   let refreshAssetGrid = false;
 
   let personName = '';
-  $: thumbnailData = getPeopleThumbnailUrl(data.person.id);
+  $: thumbnailData = getPeopleThumbnailUrl(data.person);
 
   let name: string = data.person.name;
   let suggestedPeople: PersonResponseDto[] = [];
@@ -121,7 +121,7 @@
 
     return websocketEvents.on('on_person_thumbnail', (personId: string) => {
       if (data.person.id === personId) {
-        thumbnailData = getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`;
+        thumbnailData = getPeopleThumbnailUrl(data.person, Date.now().toString());
       }
     });
   });
@@ -206,10 +206,13 @@
     if (viewMode !== ViewMode.SELECT_PERSON) {
       return;
     }
+    try {
+      await updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
+      notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info });
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_set_feature_photo'));
+    }
 
-    await updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
-
-    notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info });
     assetInteractionStore.clearMultiselect();
 
     viewMode = ViewMode.VIEW_ASSETS;
@@ -525,7 +528,7 @@
                       <ImageThumbnail
                         circle
                         shadow
-                        url={getPeopleThumbnailUrl(person.id)}
+                        url={getPeopleThumbnailUrl(person)}
                         altText={person.name}
                         widthStyle="2rem"
                         heightStyle="2rem"