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
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
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.swiftaround line 1784 (v1.6.1, commitda3a94e)Compare with
.batchRequestFailedat line 1781 which correctly re-queues:The same pattern exists for failed record deletes (around line 1820).
Steps to reproduce
tables)
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
mainbranch of this package.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.batchRequestFailedis handled:Actual behavior
The
continueskips the record without appending it tonewPendingRecordZoneChanges. Since thedeferblock only adds records in that array back tosyncEngine.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