diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js
--- a/keyserver/src/push/crypto.js
+++ b/keyserver/src/push/crypto.js
@@ -31,6 +31,7 @@
   notification: apn.Notification,
   codeVersion?: ?number,
   notificationSizeValidator?: apn.Notification => boolean,
+  blobHolder?: ?string,
 ): Promise<{
   +notification: apn.Notification,
   +payloadSizeExceeded: boolean,
@@ -47,6 +48,11 @@
 
   encryptedNotification.id = notification.id;
   encryptedNotification.payload.id = notification.id;
+
+  if (blobHolder) {
+    encryptedNotification.payload.blobHolder = blobHolder;
+  }
+
   encryptedNotification.payload.keyserverID = notification.payload.keyserverID;
   encryptedNotification.topic = notification.topic;
   encryptedNotification.sound = notification.aps.sound;
@@ -334,12 +340,13 @@
   }>,
 > {
   const notificationPromises = devices.map(
-    async ({ cookieID, deviceToken }) => {
+    async ({ cookieID, deviceToken, blobHolder }) => {
       const notif = await encryptAPNsNotification(
         cookieID,
         notification,
         codeVersion,
         notificationSizeValidator,
+        blobHolder,
       );
       return { cookieID, deviceToken, ...notif };
     },
diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js
--- a/keyserver/src/push/send.js
+++ b/keyserver/src/push/send.js
@@ -1050,11 +1050,11 @@
     notificationSizeValidator,
   );
 
-  const devicesWithExcessiveSize = notifsWithMessageInfos
+  const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos
     .filter(({ payloadSizeExceeded }) => payloadSizeExceeded)
     .map(({ deviceToken, cookieID }) => ({ deviceToken, cookieID }));
 
-  if (devicesWithExcessiveSize.length === 0) {
+  if (devicesWithExcessiveSizeNoHolders.length === 0) {
     return notifsWithMessageInfos.map(
       ({
         notification: notif,
@@ -1074,11 +1074,13 @@
     native: NEXT_CODE_VERSION,
   });
 
-  let blobHash, encryptionKey, blobUploadError;
+  let blobHash, blobHolders, encryptionKey, blobUploadError;
   if (canQueryBlobService) {
-    ({ blobHash, encryptionKey, blobUploadError } = await blobServiceUpload(
-      copyWithMessageInfos.compile(),
-    ));
+    ({ blobHash, blobHolders, encryptionKey, blobUploadError } =
+      await blobServiceUpload(
+        copyWithMessageInfos.compile(),
+        devicesWithExcessiveSizeNoHolders.length,
+      ));
   }
 
   if (blobUploadError) {
@@ -1088,12 +1090,23 @@
     );
   }
 
-  if (blobHash && encryptionKey) {
+  let devicesWithExcessiveSize = devicesWithExcessiveSizeNoHolders;
+  if (
+    blobHash &&
+    encryptionKey &&
+    blobHolders &&
+    blobHolders.length === devicesWithExcessiveSize.length
+  ) {
     notification.payload = {
       ...notification.payload,
       blobHash,
       encryptionKey,
     };
+
+    devicesWithExcessiveSize = blobHolders.map((holder, idx) => ({
+      ...devicesWithExcessiveSize[idx],
+      blobHolder: holder,
+    }));
   }
 
   const notifsWithoutMessageInfos = await prepareEncryptedAPNsNotifications(
diff --git a/keyserver/src/push/types.js b/keyserver/src/push/types.js
--- a/keyserver/src/push/types.js
+++ b/keyserver/src/push/types.js
@@ -81,4 +81,5 @@
 export type NotificationTargetDevice = {
   +cookieID: string,
   +deviceToken: string,
+  +blobHolder?: string,
 };
diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js
--- a/keyserver/src/push/utils.js
+++ b/keyserver/src/push/utils.js
@@ -28,7 +28,7 @@
   TargetedWNSNotification,
 } from './types.js';
 import { dbQuery, SQL } from '../database/database.js';
-import { upload } from '../services/blob.js';
+import { upload, assignHolder } from '../services/blob.js';
 
 const fcmTokenInvalidationErrors = new Set([
   'messaging/registration-token-not-registered',
@@ -397,23 +397,36 @@
   }
 }
 
-async function blobServiceUpload(payload: string): Promise<
+async function blobServiceUpload(
+  payload: string,
+  numberOfHolders: number,
+): Promise<
   | {
+      +blobHolders: $ReadOnlyArray<string>,
       +blobHash: string,
       +encryptionKey: string,
     }
   | { +blobUploadError: string },
 > {
-  const blobHolder = uuid.v4();
+  const blobHolders = Array.from({ length: numberOfHolders }, () => uuid.v4());
   try {
     const { encryptionKey, encryptedPayload, encryptedPayloadHash } =
       await encryptBlobPayload(payload);
-    await upload(encryptedPayload, {
+    const [blobHolder, ...additionalHolders] = blobHolders;
+
+    const uploadPromise = upload(encryptedPayload, {
       hash: encryptedPayloadHash,
       holder: blobHolder,
     });
+
+    const additionalHoldersPromises = additionalHolders.map(holder =>
+      assignHolder({ hash: encryptedPayloadHash, holder }),
+    );
+
+    await Promise.all([uploadPromise, Promise.all(additionalHoldersPromises)]);
     return {
       blobHash: encryptedPayloadHash,
+      blobHolders,
       encryptionKey,
     };
   } catch (e) {
diff --git a/native/ios/Comm/AppDelegate.mm b/native/ios/Comm/AppDelegate.mm
--- a/native/ios/Comm/AppDelegate.mm
+++ b/native/ios/Comm/AppDelegate.mm
@@ -40,6 +40,7 @@
 
 #import "CommConstants.h"
 #import "CommCoreModule.h"
+#import "CommIOSBlobClient.h"
 #import "CommMMKV.h"
 #import "CommRustModule.h"
 #import "CommUtilsModule.h"
@@ -98,6 +99,7 @@
   RCTAppSetupPrepareApp(application);
 
   [self moveMessagesToDatabase:NO];
+  [self scheduleNSEBlobsDeletion];
   [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient
                                          error:nil];
 
@@ -386,6 +388,7 @@
 
 - (void)didReceiveNewMessageInfosNSNotification:(NSNotification *)notification {
   [self moveMessagesToDatabase:YES];
+  [self scheduleNSEBlobsDeletion];
 }
 
 - (void)registerForNewMessageInfosNotifications {
@@ -404,6 +407,22 @@
       CFNotificationSuspensionBehaviorDeliverImmediately);
 }
 
+// NSE has limited time to process notifications. Therefore
+// deferable and low priority networking such as fetched
+// blob deletion from blob service should be handled by the
+// main app on a low priority background thread.
+
+- (void)scheduleNSEBlobsDeletion {
+  dispatch_async(
+      dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+        [CommIOSBlobClient.sharedInstance deleteStoredBlobs];
+      });
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application {
+  [[CommIOSBlobClient sharedInstance] cancelOngoingRequests];
+}
+
 // Copied from
 // ReactAndroid/src/main/java/com/facebook/hermes/reactexecutor/OnLoad.cpp
 static ::hermes::vm::RuntimeConfig
diff --git a/native/ios/Comm/CommIOSServices/CommIOSBlobClient.h b/native/ios/Comm/CommIOSServices/CommIOSBlobClient.h
--- a/native/ios/Comm/CommIOSServices/CommIOSBlobClient.h
+++ b/native/ios/Comm/CommIOSServices/CommIOSBlobClient.h
@@ -3,4 +3,12 @@
 @interface CommIOSBlobClient : NSObject
 + (id)sharedInstance;
 - (NSData *)getBlobSync:(NSString *)blobHash orSetError:(NSError **)error;
+- (void)deleteBlobAsyncWithHash:(NSString *)blobHash
+                      andHolder:(NSString *)blobHolder
+             withSuccessHandler:(void (^)())successHandler
+              andFailureHandler:(void (^)(NSError *))failureHandler;
+- (void)storeBlobForDeletionWithHash:(NSString *)blobHash
+                           andHolder:(NSString *)blobHolder;
+- (void)deleteStoredBlobs;
+- (void)cancelOngoingRequests;
 @end
diff --git a/native/ios/Comm/CommIOSServices/CommIOSBlobClient.mm b/native/ios/Comm/CommIOSServices/CommIOSBlobClient.mm
--- a/native/ios/Comm/CommIOSServices/CommIOSBlobClient.mm
+++ b/native/ios/Comm/CommIOSServices/CommIOSBlobClient.mm
@@ -1,4 +1,5 @@
 #import "CommIOSBlobClient.h"
+#import "CommMMKV.h"
 #import "CommSecureStore.h"
 #import "Logger.h"
 
@@ -10,6 +11,11 @@
 #endif
 
 int const blobServiceQueryTimeLimit = 15;
+const std::string mmkvBlobHolderPrefix = "BLOB_HOLDER.";
+// The blob service expects slightly different keys in
+// delete reuqest payload than we use in notif payload
+NSString *const blobServiceHashKey = @"blob_hash";
+NSString *const blobServiceHolderKey = @"holder";
 
 @interface CommIOSBlobClient ()
 @property(nonatomic, strong) NSURLSession *sharedBlobServiceSession;
@@ -112,6 +118,129 @@
   return blobContent;
 }
 
+- (void)deleteBlobAsyncWithHash:(NSString *)blobHash
+                      andHolder:(NSString *)blobHolder
+             withSuccessHandler:(void (^)())successHandler
+              andFailureHandler:(void (^)(NSError *))failureHandler {
+  NSError *authTokenError = nil;
+  NSString *authToken =
+      [CommIOSBlobClient _getAuthTokenOrSetError:&authTokenError];
+
+  if (authTokenError) {
+    comm::Logger::log(
+        "Failed to create blob service auth token. Reason: " +
+        std::string([authTokenError.localizedDescription UTF8String]));
+    return;
+  }
+
+  NSString *blobUrlStr = [blobServiceAddress stringByAppendingString:@"/blob"];
+  NSURL *blobUrl = [NSURL URLWithString:blobUrlStr];
+
+  NSMutableURLRequest *deleteRequest =
+      [NSMutableURLRequest requestWithURL:blobUrl];
+
+  [deleteRequest setValue:authToken forHTTPHeaderField:@"Authorization"];
+  [deleteRequest setValue:@"application/json"
+       forHTTPHeaderField:@"content-type"];
+
+  deleteRequest.HTTPMethod = @"DELETE";
+  deleteRequest.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{
+    blobServiceHolderKey : blobHolder,
+    blobServiceHashKey : blobHash
+  }
+                                                           options:0
+                                                             error:nil];
+
+  NSURLSessionDataTask *task = [self.sharedBlobServiceSession
+      dataTaskWithRequest:deleteRequest
+        completionHandler:^(
+            NSData *_Nullable data,
+            NSURLResponse *_Nullable response,
+            NSError *_Nullable error) {
+          NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
+
+          if (httpResponse.statusCode > 299) {
+            NSString *errorMessage =
+                [@"Deleting blob failed with the following reason: "
+                    stringByAppendingString:[NSHTTPURLResponse
+                                                localizedStringForStatusCode:
+                                                    httpResponse.statusCode]];
+            failureHandler([NSError
+                errorWithDomain:@"app.comm"
+                           code:httpResponse.statusCode
+                       userInfo:@{NSLocalizedDescriptionKey : errorMessage}]);
+            return;
+          }
+
+          if (error) {
+            failureHandler(error);
+            return;
+          }
+
+          successHandler();
+        }];
+
+  [task resume];
+}
+
+- (void)storeBlobForDeletionWithHash:(NSString *)blobHash
+                           andHolder:(NSString *)blobHolder {
+  std::string blobHashCpp = std::string([blobHash UTF8String]);
+  std::string blobHolderCpp = std::string([blobHolder UTF8String]);
+
+  std::string mmkvBlobHolderKey = mmkvBlobHolderPrefix + blobHolderCpp;
+  comm::CommMMKV::setString(mmkvBlobHolderKey, blobHashCpp);
+}
+
+- (void)deleteStoredBlobs {
+  std::vector<std::string> allKeys = comm::CommMMKV::getAllKeys();
+  NSMutableArray<NSDictionary *> *blobsDataForDeletion =
+      [[NSMutableArray alloc] init];
+
+  for (const auto &key : allKeys) {
+    if (key.size() <= mmkvBlobHolderPrefix.size() ||
+        key.compare(0, mmkvBlobHolderPrefix.size(), mmkvBlobHolderPrefix)) {
+      continue;
+    }
+
+    std::optional<std::string> blobHash = comm::CommMMKV::getString(key);
+    if (!blobHash.has_value()) {
+      continue;
+    }
+    std::string blobHolder = key.substr(mmkvBlobHolderPrefix.size());
+
+    NSString *blobHolderObjC =
+        [NSString stringWithCString:blobHolder.c_str()
+                           encoding:NSUTF8StringEncoding];
+    NSString *blobHashObjC =
+        [NSString stringWithCString:blobHash.value().c_str()
+                           encoding:NSUTF8StringEncoding];
+
+    [self deleteBlobAsyncWithHash:blobHashObjC
+        andHolder:blobHolderObjC
+        withSuccessHandler:^{
+          std::string mmkvBlobHolderKey = mmkvBlobHolderPrefix + blobHolder;
+          comm::CommMMKV::removeKeys({mmkvBlobHolderKey});
+        }
+        andFailureHandler:^(NSError *error) {
+          comm::Logger::log(
+              "Failed to delete blob hash " + blobHash.value() +
+              " from blob service. Details: " +
+              std::string([error.localizedDescription UTF8String]));
+        }];
+  }
+}
+
+- (void)cancelOngoingRequests {
+  [self.sharedBlobServiceSession
+      getAllTasksWithCompletionHandler:^(
+          NSArray<__kindof NSURLSessionTask *> *_Nonnull tasks) {
+        for (NSURLSessionTask *task in tasks) {
+          [task cancel];
+        }
+      }];
+}
+
 + (NSString *)_getAuthTokenOrSetError:(NSError **)error {
   // Authentication data are retrieved on every request
   // since they might change while NSE process is running
diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm
--- a/native/ios/NotificationService/NotificationService.mm
+++ b/native/ios/NotificationService/NotificationService.mm
@@ -17,6 +17,7 @@
 NSString *const collapseIDKey = @"collapseID";
 NSString *const keyserverIDKey = @"keyserverID";
 NSString *const blobHashKey = @"blobHash";
+NSString *const blobHolderKey = @"blobHolder";
 NSString *const encryptionKeyLabel = @"encryptionKey";
 
 // Those and future MMKV-related constants should match
@@ -553,6 +554,9 @@
       [NotificationService aesDecryptAndParse:largePayloadBinary
                                       withKey:encryptionKey];
   [self persistMessagePayload:largePayload];
+  [CommIOSBlobClient.sharedInstance
+      storeBlobForDeletionWithHash:blobHash
+                         andHolder:content.userInfo[blobHolderKey]];
 }
 
 - (BOOL)needsSilentBadgeUpdate:(NSDictionary *)payload {
@@ -575,7 +579,8 @@
 }
 
 - (BOOL)isLargeNotification:(NSDictionary *)payload {
-  return payload[blobHashKey] && payload[encryptionKeyLabel];
+  return payload[blobHashKey] && payload[encryptionKeyLabel] &&
+      payload[blobHolderKey];
 }
 
 - (UNNotificationContent *)getBadgeOnlyContentFor: