Skip to content

App hang (5000+ ms) in LDClient.didEnterBackground — synchronous NSUserDefaults.set blocks main thread #488

@karolkulesza

Description

@karolkulesza

Summary

LDClient.didEnterBackground causes a main-thread hang of 5000+ ms by synchronously writing to NSUserDefaults during the background transition. This is affecting 653 users across 7,016 occurrences in our production app.

This is the same root cause reported in #398, which was closed without a code fix.

Environment

  • SDK versions affected: 9.12.2 and 11.1.1 (confirmed identical code path in both)
  • iOS versions: All tested — iOS 18.6.2 through 26.3.1
  • Devices: Primarily iPad (iPad13,19, iPad12,1, and others)
  • App: Enterprise sales enablement app deployed to managed iPad fleet

Reproduction

The hang occurs when the app transitions to background (Home button, app switch, or device lock). It requires concurrent NSUserDefaults access from other subsystems at the moment of backgrounding — common on MDM-managed devices and apps that use NSUserDefaults for caching.

Root Cause

The full blocking call chain on the main thread:

  1. UIApplication._applicationDidEnterBackground posts notification
  2. LDClient.didEnterBackground (LDClient.swift:262) observes it
  3. NSThread.performOnMainDispatchQueue.main.sync (Thread.swift)
  4. Sets LDClient.runMode = .background triggering didSet (LDClient.swift:214)
  5. Updates connectionInformation triggering didSet (LDClient.swift:230)
  6. ConnectionInformationStore.storeConnectionInformation() (ConnectionInformationStore.swift:11)
  7. NSUserDefaults.save<T>NSUserDefaults.set (KeyedValueCache.swift:15)
  8. NSUserDefaults.set internally calls -[NSOperation waitUntilFinished]blocks on _pthread_cond_wait

The issue is that NSUserDefaults.set() can trigger a cross-process synchronization that blocks when the system is already coordinating the background transition. This is exacerbated on managed (MDM) devices where system-level NSUserDefaults observers add contention.

Stacktrace (from Sentry)

LDClient.didEnterBackground (LDClient.swift:262)
  NSThread.performOnMain (<compiler-generated>)
    NSThread.performOnMain (Thread.swift:12)
      closure in LDClient.didEnterBackground (LDClient.swift:263)
        LDClient.runMode.setter (<compiler-generated>)
          LDClient.runMode.didset (LDClient.swift:214)
            LDClient.connectionInformation.setter (<compiler-generated>)
              LDClient.connectionInformation.didset (LDClient.swift:230)
                ConnectionInformationStore.storeConnectionInformation (<compiler-generated>)
                  ConnectionInformationStore.storeConnectionInformation (ConnectionInformationStore.swift:11)
                    NSUserDefaults.save<T> (ConnectionInformationStore.swift:19)
                      NSUserDefaults.set (KeyedValueCache.swift:15)
                        -[NSNotificationCenter postNotificationName:object:userInfo:]
                          -[NSOperation waitUntilFinished]
                            _pthread_cond_wait
                              __psynch_cvwait  ← BLOCKED

Impact

Metric Value
Total occurrences 7,016
Unique users affected 653
First seen 2025-12-18
Last seen 2026-03-16 (ongoing)
Hang duration 5,000+ ms (Watchdog threshold)

Suggested Fix

Either of these would resolve the issue:

  1. Make ConnectionInformationStore.storeConnectionInformation() asynchronous — dispatch the UserDefaults.set call to a background queue
  2. Change Thread.performOnMain in didEnterBackground to use DispatchQueue.main.async instead of DispatchQueue.main.sync
  3. Offer an alternative to NSUserDefaults for connection info persistence (related: Different Feature Flag Caching Mechanisms #316)

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions