DistributedNotificationCenter on macOS: Coalescing, Suspension, and Trust Boundaries
Design cross-process macOS notifications with explicit suspension behavior, coalescing expectations, property-list payload limits, and untrusted-input handling.
Foundation’s DistributedNotificationCenter is a same-machine broadcast mechanism for processes that need to announce that something changed. It is not the same as the ordinary NotificationCenter, whose default center broadcasts only inside one program. A distributed notification crosses a task boundary through a system-wide server, but it does not become a reliable queue, authenticated command channel, or request/reply protocol as a result.
That distinction is the core design rule: send a small invalidation hint, then let the receiver read current state from an authoritative store. Do not make correctness depend on receiving every post, receiving one within a deadline, or trusting the sender’s payload. Apple explicitly documents unbounded delivery latency, possible drops when the server queue fills, property-list serialization for userInfo, and the absence of a secure communications protocol.
Scope the mechanism before choosing it
The default distributed center represents the local computer. It broadcasts to other tasks that registered with their own default center; it is not a network notification service. Apple describes posting as relatively expensive because a system-wide server distributes each post to registered tasks. The receiver must also have a run loop running in a common mode to receive notifications. In modern macOS, delivery for multithreaded applications is on the main thread, so a callback should stay short and must not block UI responsiveness.
Use a process-local NotificationCenter for coordination within one process. Use distributed notifications when loosely coupled local processes need a low-payload “something may have changed” signal. If the sender needs a reply, the receiver must authorize a peer, or work must continue reliably after a client exits, use a purpose-built IPC mechanism. Apple’s XPC APIs provide connections for structured messages and replies, and can apply peer identity requirements; they are a better fit for command semantics than a broadcast notification.
Suspension behavior changes what survives
An observer registration can specify what the server should do while the receiving notification center is suspended. The policies are materially different:
| Behavior | What Apple documents | Appropriate interpretation |
|---|---|---|
drop |
Do not queue matching notifications while delivery is suspended | A transient hint may disappear entirely |
coalesce |
Keep only the last notification with the same name and object; drop earlier ones | Intermediate states are intentionally lost |
hold |
Queue matching notifications until the server queue fills, when it may flush them | Not an unbounded or durable event log |
deliverImmediately |
Flush queued notifications, deliver this notification, then restore the prior suspension state | An override of suspension, not an acknowledgment or persistence guarantee |
When a registration does not explicitly choose a suspension policy, Apple documents coalescing as the default. Coalescing matches the notification name and sender object; changing the object can therefore change which posts collapse together. NSNotificationQueue has its own local coalescing rules and should not be confused with the distributed server’s suspension behavior.
AppKit automatically suspends distributed notification delivery when an application is inactive. AppKit applications should normally let NSApplication manage that lifecycle. Foundation-only tools may need to manage the center’s suspended property themselves. Either way, a consumer must handle waking with one recent hint, or no hint, and then reload the current state.
Register narrowly and publish a hint
The selector-based API makes the suspension policy explicit. Keep the sender object and notification name stable when you want equivalent state-change notices to coalesce.
import Foundation
final class CacheObserver: NSObject {
@objc func cacheMayHaveChanged(_ notification: Notification) {
// Treat this as an invalidation hint. Read the current cache snapshot
// from its owner instead of assuming this callback contains the truth.
refreshFromAuthoritativeStore()
}
private func refreshFromAuthoritativeStore() {
// Application-defined, bounded, nonblocking refresh.
}
}
let center = DistributedNotificationCenter.default()
let observer = CacheObserver() // Retain for as long as observation is needed.
let name = Notification.Name("com.example.catalog.cache-changed")
let sender = "com.example.catalog"
center.addObserver(
observer,
selector: #selector(CacheObserver.cacheMayHaveChanged(_:)),
name: name,
object: sender,
suspensionBehavior: .coalesce
)
// A sandboxed sender must pass nil for userInfo.
center.post(name: name, object: sender, userInfo: nil)
The store refresh in this example is deliberately application-defined. A notification callback should not synchronously fetch a network resource, walk a large directory, or perform a privileged mutation. Retain the observer while registered, remove it during teardown, and make repeated refresh requests idempotent. If the store is already current, the hint should be cheap to ignore.
Treat the envelope as untrusted input
Apple says distributed notifications do not implement a secure communications protocol and instructs applications to treat passed data as untrusted. A notification name or object string is a routing filter, not proof of the sender’s code identity or authorization. Any receiving process that accepts userInfo should validate required keys, types, sizes, version fields, and semantic ranges before using them. Never let a notification payload alone authorize a destructive operation, choose an arbitrary executable or path, or convey a secret.
The userInfo dictionary is serialized as a property list before it crosses the task boundary, which limits its representable values. More importantly, Apple’s API documentation says a sandboxed sending application can post only when the dictionary is absent: its userInfo must be nil. A design that depends on a sandboxed process sending arbitrary dictionaries is therefore invalid. Prefer a compact notice that tells the receiver to read shared, access-controlled state through the application’s chosen storage or IPC boundary.
The postToAllSessions option expands delivery beyond applications in the sender’s login session. Do not enable it casually: each extra session is another receiver population, and the data remains an untrusted broadcast rather than an authenticated message. If cross-session communication is required, define what each receiver is allowed to learn and how it verifies current authorization independently.
Build for loss, delay, and duplicate refreshes
An operationally sound pattern is “write state, then post an invalidation hint.” The durable state change belongs in the store or service that owns it. Receivers should periodically or on activation reconcile with that source, because the notification server can drop posts and delivery latency is not bounded. Include a monotonically increasing revision in shared state if consumers need to detect that their cached snapshot is stale; do not treat notification count as a revision log.
Test behavior while the receiver is active, inactive, suspended, resumed, and restarting. Post several same-name/same-object notices during suspension and verify that coalescing causes one state refresh to observe the latest stored state. Test drop separately, verify hold under queue pressure is not relied on for completeness, and avoid using deliverImmediately as a substitute for recovery. Confirm a missing notification is repaired by startup reconciliation and that malformed or forged payloads cannot trigger an operation.
Use DistributedNotificationCenter for lightweight local broadcast and cache invalidation. Use XPC or another explicitly designed IPC protocol when you need structured requests, replies, caller checks, or operation results. Keeping that line clear prevents a convenient notification API from quietly becoming an unreliable and under-protected control plane.
Related:
- NotificationCenter on macOS: Observer Ownership, Delivery, and Reentrancy
- XPC Services: How macOS Processes Talk to Each Other Securely
Sources:
- Apple: DistributedNotificationCenter overview, delivery limits, and trust warning
- Apple: DistributedNotificationCenter suspension behaviors
- Apple: Suspending and resuming distributed notification delivery
- Apple: Posting distributed notifications and property-list payload constraints
- Apple: NotificationCenter versus distributed notifications
- Apple: XPC interprocess communication
- Apple: Require a code-signing identity for an XPC connection