diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj
index 1aa154f5d..13afe2788 100644
--- a/Session.xcodeproj/project.pbxproj
+++ b/Session.xcodeproj/project.pbxproj
@@ -6814,7 +6814,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				CODE_SIGN_STYLE = Automatic;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 423;
+				CURRENT_PROJECT_VERSION = 424;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEVELOPMENT_TEAM = SUQ8J2PCT7;
 				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@@ -6886,7 +6886,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				CODE_SIGN_STYLE = Automatic;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 423;
+				CURRENT_PROJECT_VERSION = 424;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEVELOPMENT_TEAM = SUQ8J2PCT7;
 				ENABLE_NS_ASSERTIONS = NO;
@@ -6951,7 +6951,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				CODE_SIGN_STYLE = Automatic;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 423;
+				CURRENT_PROJECT_VERSION = 424;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEVELOPMENT_TEAM = SUQ8J2PCT7;
 				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@@ -7025,7 +7025,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				CODE_SIGN_STYLE = Automatic;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 423;
+				CURRENT_PROJECT_VERSION = 424;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEVELOPMENT_TEAM = SUQ8J2PCT7;
 				ENABLE_NS_ASSERTIONS = NO;
@@ -7985,7 +7985,7 @@
 				CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
 				CODE_SIGN_IDENTITY = "iPhone Developer";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				CURRENT_PROJECT_VERSION = 423;
+				CURRENT_PROJECT_VERSION = 424;
 				DEVELOPMENT_TEAM = SUQ8J2PCT7;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
@@ -8056,7 +8056,7 @@
 				CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
 				CODE_SIGN_IDENTITY = "iPhone Developer";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				CURRENT_PROJECT_VERSION = 423;
+				CURRENT_PROJECT_VERSION = 424;
 				DEVELOPMENT_TEAM = SUQ8J2PCT7;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift
index e4bda9df5..d75a31149 100644
--- a/Session/Notifications/SyncPushTokensJob.swift
+++ b/Session/Notifications/SyncPushTokensJob.swift
@@ -86,7 +86,36 @@ public enum SyncPushTokensJob: JobExecutor {
         Logger.info("Re-registering for remote notifications.")
         PushRegistrationManager.shared.requestPushTokens()
             .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<Void, Error> in
-                PushNotificationAPI
+                /// For our `subscribe` endpoint we only want to call it if:
+                /// • It's been longer than `SyncPushTokensJob.maxFrequency` since the last subscription;
+                /// • The token has changed; or
+                /// • We want to force an update
+                let timeSinceLastSubscription: TimeInterval = dependencies.dateNow
+                    .timeIntervalSince(
+                        dependencies.standardUserDefaults[.lastPushNotificationSync]
+                            .defaulting(to: Date.distantPast)
+                    )
+                let uploadOnlyIfStale: Bool? = {
+                    guard
+                        let detailsData: Data = job.details,
+                        let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
+                    else { return nil }
+                    
+                    return details.uploadOnlyIfStale
+                }()
+                
+                guard
+                    timeSinceLastSubscription >= SyncPushTokensJob.maxFrequency ||
+                    dependencies.storage[.lastRecordedPushToken] != pushToken ||
+                    uploadOnlyIfStale == false
+                else {
+                    SNLog("[SyncPushTokensJob] OS subscription completed, skipping server subscription due to frequency")
+                    return Just(())
+                        .setFailureType(to: Error.self)
+                        .eraseToAnyPublisher()
+                }
+                
+                return PushNotificationAPI
                     .subscribeAll(
                         token: Data(hex: pushToken),
                         isForcedUpdate: true,
diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift
index fc9e93f9e..41fdf6b52 100644
--- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift	
+++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift	
@@ -8,6 +8,8 @@ extension PushNotificationAPI {
             case accountId = "@"
             case hash = "#"
             case namespace = "n"
+            case createdTimestampMs = "t"
+            case expirationTimestampMs = "z"
             case dataLength = "l"
             case dataTooLong = "B"
         }
@@ -21,6 +23,12 @@ extension PushNotificationAPI {
         /// The swarm namespace in which this message arrived.
         let namespace: Int
         
+        /// The swarm timestamp when the message was created (unix epoch milliseconds)
+        let createdTimestampMs: Int64
+        
+        /// The message's swarm expiry timestamp (unix epoch milliseconds)
+        let expirationTimestampMs: Int64
+        
         /// The length of the message data.  This is always included, even if the message content
         /// itself was too large to fit into the push notification.
         let dataLength: Int
@@ -40,6 +48,8 @@ extension PushNotificationAPI.NotificationMetadata {
             accountId: try container.decode(String.self, forKey: .accountId),
             hash: try container.decode(String.self, forKey: .hash),
             namespace: try container.decode(Int.self, forKey: .namespace),
+            createdTimestampMs: try container.decode(Int64.self, forKey: .createdTimestampMs),
+            expirationTimestampMs: try container.decode(Int64.self, forKey: .expirationTimestampMs),
             dataLength: try container.decode(Int.self, forKey: .dataLength),
             dataTooLong: ((try? container.decode(Int.self, forKey: .dataTooLong) != 0) ?? false)
         )