Skip to content

[Bug] SyncBackoffService never triggered, causing infinite retry loops on sync errors #4071

@chrislyons

Description

@chrislyons

Summary

The SyncBackoffService is fully implemented with exponential backoff logic, but backoffItem() is never called anywhere in production code. When sync errors occur (429 rate limiting, content limit errors, or other server errors), items remain in the dirty queue and immediately retry on the next sync, creating an infinite loop.

Environment

  • Standard Notes version: Latest (tested against commit ce1fbe4)
  • Platform: All (Web, Desktop, Mobile)

User-Reported Symptoms

Users experiencing sync failures report:

  • "Too many requests. Please try again later." (429 errors)
  • "You have exceeded your content limit. Please upgrade your account." (even with paid Pro accounts)
  • Sign out/sign in temporarily resolves the issue
  • Emptying trash sometimes helps
  • Sync eventually becomes completely non-functional

Steps to Reproduce

  1. Trigger a sync error (rate limit, content limit, etc.)
  2. Observe that:
    • Error toast is shown
    • Items remain in dirty queue
    • Next sync immediately retries the same items
    • Same error occurs
    • Loop continues indefinitely

Root Cause Analysis

backoffItem() Never Called

The SyncBackoffService at packages/services/src/Domain/Sync/SyncBackoffService.ts is:

  • ✅ Created and injected at Dependencies.ts:1361
  • ✅ Used to filter items in SyncService.itemsNeedingSync() at line 459
  • backoffItem() is never called in production code
# Verification:
grep -r "\.backoffItem\(" packages/ --include="*.ts" | grep -v ".spec.ts" | grep -v ".test.js"
# Result: Only the definition - no production callers

Only Two Status Codes Specifically Handled

In SyncService.ts, only these errors have special handling:

  • 401 (Invalid Session) - triggers re-auth
  • 429 (Too Many Requests) - shows toast

All other errors (400, 403, 500, content limit, etc.) have no backoff logic.

Sync Operation Continues After Errors

AccountSyncOperation.run() continues making HTTP requests even after receiving error responses, potentially exacerbating rate limits.

Expected Behavior

  1. Items that fail to sync should be backed off exponentially (1s → 2s → 4s → 8s...)
  2. The sync operation should stop making requests when rate limited
  3. Non-failing items should continue syncing normally
  4. Failed items retry only after backoff expires

Actual Behavior

  1. Failed items immediately retry on every sync attempt
  2. Operation continues making requests after errors
  3. Creates thundering herd effect
  4. Eventually breaks sync completely

Impact

  • Severity: High
  • User Impact: Sync becomes non-functional, requiring sign-out/sign-in workaround
  • Data Risk: Changes don't sync across devices

Proposed Fix

Wire up the existing SyncBackoffService:

  1. Call operation.abort() when rate limited to stop further requests
  2. Call backoffItem() for all items in failed sync operations
  3. Consider extending backoff to other error types (403, 500, etc.)

Additional Context

The backoff service is well-tested (SyncBackoffService.spec.ts) - only the integration is missing.

Related Reports

  • Forum #3699 - "Too many requests" errors
  • Users report sign-out/sign-in temporarily fixes the issue (resets dirty state)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions