-
Notifications
You must be signed in to change notification settings - Fork 97
Description
Description
When CloudKit returns transient errors like .serviceUnavailable (CKError 6/2009) or .requestRateLimited (CKError 7/2062)for individual record saves, the SyncEngine silently drops those records from the sync queue. They are never retried, causing permanent data loss from the sync perspective.
Location: SyncEngine.swift around line 1784 (v1.6.1, commit da3a94e)
case .networkFailure, .networkUnavailable, .zoneBusy, .serviceUnavailable,
.notAuthenticated, .operationCancelled,
.internalError, .partialFailure, .badContainer, .requestRateLimited, .missingEntitlement,
.invalidArguments, .resultsTruncated, .assetFileNotFound,
.assetFileModified, .incompatibleVersion, .constraintViolation, .changeTokenExpired,
.badDatabase, .quotaExceeded, .limitExceeded, .userDeletedZone, .tooManyParticipants,
.alreadyShared, .managedAccountRestricted, .participantMayNeedVerification,
.serverResponseLost, .assetNotAvailable, .accountTemporarilyUnavailable:
continue // ← record is NOT re-queued
Compare with .batchRequestFailed at line 1781 which correctly re-queues:
case .batchRequestFailed:
newPendingRecordZoneChanges.append(.saveRecord(failedRecord.recordID))
break
The same pattern exists for failed record deletes (around line 1820).
Steps to reproduce
- Register multiple tables with SyncEngine (in my case ~25 tables including junction tables)
- Save a record that triggers writes across several tables (e.g., a parent record + genres + credits via junction
tables) - CloudKit starts rate-limiting due to the burst of CKRecord operations
- Observe that throttled records are never synced
Console output:
"
Caught error: <CKError 0x10b6fccc0: "Service Unavailable" (6/2009); Retry after 13.0 seconds; container ID = container_id>: SQLiteData CloudKit Failure
SQLiteData (private.db) willFetchRecordZoneChanges
✅ Zone: co.pointfree.SQLiteData.defaultZone:defaultOwner
❌ requestRateLimited
error fetching changes for context :
<CKError 0x1313b4330: "Request Rate Limited" (7/2062);
"Operation throttled by previous server http 429 reply. Retry after 11.0 seconds.">;
"
Checklist
- I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
- I have determined whether this bug is also reproducible in a vanilla GRDB project.
- If possible, I've reproduced the issue using the
mainbranch of this package. - This issue hasn't been addressed in an existing GitHub issue or discussion.
Expected behavior
Transient errors (at minimum .serviceUnavailable, .requestRateLimited, .zoneBusy, .networkFailure, .networkUnavailable) should re-queue the failed record so CKSyncEngine can retry it, similar to how .batchRequestFailed is handled:
case .serviceUnavailable, .requestRateLimited, .zoneBusy,
.networkFailure, .networkUnavailable:
newPendingRecordZoneChanges.append(.saveRecord(failedRecord.recordID))
Actual behavior
The continue skips the record without appending it to newPendingRecordZoneChanges. Since the defer block only adds records in that array back to syncEngine.state, the dropped records are permanently lost from the sync queue.
Reproducing project
In apps with many related tables, a single save operation (e.g., movie + 5 genres + 10 credits = 16 records) easily triggers CloudKit rate limiting. Once throttling begins, it cascades — more records get dropped, more retries fail, and data never syncs. Calling syncEngine.syncChanges() doesn't help because the records are no longer tracked as pending.
SQLiteData version information
1.6.1 (da3a94e)
Sharing version information
2.7.4
GRDB version information
7.10.0
Destination operating system
iOS 26
Xcode version information
26.4
Swift Compiler version information
Swift 6