Answer
The core idea
UserDefaults is the system interface to the defaults database: a persistent key-value store for app-specific, nonsensitive settings. Values must be property-list types (Bool, numbers, String, Data, Date, URL, and arrays or dictionaries of those). The API is built for preferences and flags, not for documents, caches, or secrets.
Under the hood each domain lives as a property list that the process keeps in memory and flushes to disk asynchronously. A custom suiteName points at a different database file — including App Group suites shared with extensions.
The short interview answer is: keep UserDefaults tiny (flags and settings), expect the whole domain in memory with async plist writes, and treat each suite as its own database.
1. How much data can you store?
There is no single public “hard cap” documented for local iOS UserDefaults the way there is for iCloud key-value storage. In practice the product rule is stricter than the API: store small values — feature flags, last selected tab, a short JSON blob of settings — measured in kilobytes, not megabytes.
From iOS 13 / modern CFPreferences onward, attempting to put roughly 4 MB (4_194_304 bytes) or more into a defaults domain logs a Console warning (Attempting to store >= 4194304 bytes of data in CFPreferences/NSUserDefaults…) and can post sizeLimitExceededNotification. tvOS is stricter still: the historical guidance is a warning around 512 KB and process termination around 1 MB.
Stuffing a large payload is the anti-pattern:
let imageData = try Data(contentsOf: photoURL) // multi-MB
UserDefaults.standard.set(imageData, forKey: "avatar") // ❌ Wrong store for a blobPrefer a file (or a database) and keep only a path or identifier in defaults:
try imageData.write(to: avatarFileURL, options: .atomic)
UserDefaults.standard.set(avatarFileURL.path, forKey: "avatarPath") // ✅ Small preferenceSecrets (tokens, passwords) belong in the Keychain, not UserDefaults — defaults are not encrypted for confidentiality and are included in device backups.
2. How UserDefaults stores data on disk
A set updates the in-memory representation immediately. Persistence to disk is asynchronous, mediated by CFPreferences (and the cfprefsd preferences daemon on Apple platforms). Callers do not need synchronize() in normal app code; that method is deprecated because the system already flushes on a schedule and at process milestones.
For UserDefaults.standard, the persistent file for the app domain is under the sandbox:
Library/Preferences/<bundle-identifier>.plistThe domain is managed as a unit: the preferences system loads the plist into memory and writes the domain back as a whole when it flushes. That is why a multi-megabyte value hurts — every meaningful change can involve rewriting a large on-disk representation, and the process pays the memory cost of holding the domain.
Values written through UserDefaults are device-local prefs (backed up with the device). They are not a sync channel between the user’s devices; that role belongs to APIs like NSUbiquitousKeyValueStore when iCloud key-value storage is appropriate.
3. How suiteName changes where data lives
UserDefaults.standard reads and writes the app’s own persistent domain (tied to the bundle identifier). UserDefaults(suiteName:) opens a different defaults database named by that suite string. Writes go to that suite’s plist, not to the standard app domain.
App Groups use a suite whose name matches the group identifier (group.com.example.app). The main app and an extension each construct the same suite and share that database inside the group container:
let shared = UserDefaults(suiteName: "group.com.example.app")
shared?.set(true, forKey: "hasCompletedOnboarding")
let standard = UserDefaults.standard
standard.set(true, forKey: "hasCompletedOnboarding")
// ✅ Same key string, different databases — they do not overwrite each otheraddSuite(named:) is a different tool. It inserts another domain into the search list of an existing UserDefaults object so reads can fall through to that suite. Writes still apply to the object’s own domain. Opening UserDefaults(suiteName:) is what you use when the suite is the primary store you intend to mutate.
register(defaults:) only fills the in-memory registration domain. Those fallbacks are not written to disk; the app must register them again on each launch.
4. Can you read it on the main thread?
Yes. Apple documents UserDefaults as thread-safe: the same object can be used from multiple threads or tasks. Reading a small preference on the main thread during view setup is normal and expected.
Thread-safe does not mean free. After the domain is loaded, reads usually hit the in-memory cache and stay cheap. A huge domain, a burst of large writes from UI code, or thrashing keys on every scroll frame can still hitch the main thread because of memory pressure and flush work. The fix is not “never touch UserDefaults on main” — it is keep the payload small and avoid write storms from hot UI paths.
func applyTheme() {
let style = UserDefaults.standard.string(forKey: "theme") ?? "system"
// ✅ Fine on the main thread for a tiny string preference
window?.overrideUserInterfaceStyle = style == "dark" ? .dark : .unspecified
}5. Why it matters
- a large blob in defaults inflates memory and disk rewrite cost, which shows up as launch jank and scroll hitching
- writing to
standardwhile an extension reads an App Group suite looks like “prefs never sync” when the bug is two separate databases addSuitevsinit(suiteName:)confusion produces reads that see shared data and writes that never land where the extension looks- storing secrets in defaults exposes them to backup and to anyone who can read the sandbox prefs file
Interview angle
Start with the use case: UserDefaults is for small, nonsensitive preferences, not documents or tokens. Give the size guidance next — keep values tiny; around 4 MB CFPreferences warns on modern platforms, and tvOS is stricter — then explain the storage model: immediate in-memory update, asynchronous plist flush under Library/Preferences, whole-domain write. Cover suites: standard vs UserDefaults(suiteName:) as separate databases, App Groups for extension sharing, and addSuite as search-list only. Close with thread safety: main-thread reads are allowed and common, but the cost stays low only while the domain stays small. The UserDefaults documentation is the primary reference for the API and notifications.