Skip to content
AF
HealthKit Queries

HKObserverQuery and enableBackgroundDelivery: how the pair works

Last verified October 3, 2026 · 6 min read

HKObserverQuery is a long-running query that tells you matching HealthKit data changed, and never what changed, so its update handler normally runs an HKAnchoredObjectQuery to fetch the new and deleted samples. By default, Apple says, it only returns results while your app is in the foreground; to be woken in the background you also call HKHealthStore's enableBackgroundDelivery(for:frequency:withCompletion:), where the frequency is a maximum, not a schedule. When a background delivery wakes you, Apple requires you to call the update handler's completion handler once you have processed the data, and says that if your app fails to respond three times HealthKit stops sending background updates.

Covered here:HealthKit

This page explains how the two halves of HealthKit's change notification fit together, according to Apple's documentation. If yours is not firing at all, start with the diagnosis on HealthKit background delivery not working, which covers the entitlement, the Simulator and the other gates in order. For what to build around it when wakes are late or missing, see background sync.

What Apple says it is#

HKObserverQuery: "A long-running query that monitors the HealthKit store and updates your app when the HealthKit store saves or deletes a matching sample." The overview: "Observer queries set up a long-running task on a background queue. This task watches the HealthKit store, and alerts you when the store saves or removes matching data. Your app uses observer queries to respond to changes made by other apps and devices."

Like the other classes, it is immutable once created. Unlike most of them, it never finishes by itself: it runs until you pass it to the store's stop(_:).

Availability. Apple lists HKObserverQuery from iOS 8.0, iPadOS 8.0, Mac Catalyst 13.0, macOS 13.0, visionOS 1.0 and watchOS 2.0. The multi-type initializer init(queryDescriptors:updateHandler:) is listed from iOS 15.0 and watchOS 8.0.

The update handler carries no data#

init(sampleType:predicate:updateHandler:) takes a sample type, an optional predicate and a handler with three parameters: the query, a completion handler and an error. There is no array of samples. Apple's article is blunt about it: "The observer query's update handler does not receive any information about the change—just that a change occurred. You must execute another query, for example an HKSampleQuery or HKAnchoredObjectQuery, to access the changes."

The anchored query is the natural partner because it returns only what changed since your saved anchor, including deletions. Apple's initializer discussion points the same way: "you can use Anchored Object Queries to retrieve the list of new samples that have been added to the store." See HKAnchoredObjectQuery.

With init(queryDescriptors:updateHandler:) you can watch several types with one query, and the handler gains a Set<HKSampleType>? parameter, which Apple describes as "A list of data types for all of the changed objects". That tells you which types to re-read, still not which samples.

The completion handler contract#

The second handler parameter is an HKObserverQueryCompletionHandler, a closure with no arguments. Apple's parameter description sets out both halves of the rule: "If you have registered for background updates, you must call this completion handler as soon as you are done processing the incoming data. This tells HealthKit that you have successfully received the background update. Additionally, you should only call the completion handler when you are using background updates."

The consequence of skipping it, from the type alias's own page: "If you do not call this block, HealthKit continues to attempt to launch your app using a back off algorithm. If your app fails to respond three times, HealthKit assumes that your app cannot receive data, and stops sending you background updates."

enableBackgroundDelivery(for:frequency:withCompletion:)#

Apple's article: "By default, observer queries only return results while your app is in the foreground; however, you can configure your app to also receive update notifications in the background." The switch is a method on HKHealthStore, "Enables the delivery of updates to an app running in the background." There is also an async throws form without the completion closure.

  • type: "This object can be a HKCharacteristicType, HKQuantityType, HKCategoryType, or HKWorkoutType. HKCorrelationType is not a supported type for background delivery."
  • frequency: "The maximum frequency of the updates. The system wakes your app from the background at most once per time period specified."
  • completion: receives success and an error.

Apple's documentation data lists this method on Mac Catalyst 13.0, macOS 13.0 and watchOS 8.0, and lists iOS, iPadOS and visionOS without an introduction version. The entitlement the method requires, and the error it returns without it, are covered in the HealthKit integration guide and the troubleshooting page above.

HKUpdateFrequencyApple's description
.immediate"The system launches your app every time it detects a change."
.hourly"The system launches your app at most once an hour in response to changes."
.daily"The system launches your app at most once a day in response to changes."
.weekly"The system launches your app at most once per week in response to changes."

Every value except .immediate is a ceiling, and Apple adds that "Some sample types have a maximum frequency of HKUpdateFrequency.hourly. The system enforces this frequency transparently." The cap is applied for you, whatever you passed.

Where the query has to live#

When HealthKit wakes your app for a background delivery, "HealthKit calls the update handler for any observer queries that match the newly saved data." So the query must already exist. Apple's instruction: "If you plan on supporting background delivery, set up all your observer queries in your app delegate's application(_:didFinishLaunchingWithOptions:) method."

The pair in code#

import HealthKit

func startObservingSteps(store: HKHealthStore) {
    let stepType = HKQuantityType(.stepCount)

    let query = HKObserverQuery(sampleType: stepType, predicate: nil) { _, completionHandler, error in
        if error != nil {
            // Apple's own sample returns here without calling completionHandler;
            // Apple does not document what to do on the error path.
            return
        }
        // The handler says only that something changed. Fetch the changes:
        runAnchoredSync {            // your code: an HKAnchoredObjectQuery with your saved anchor
            completionHandler()      // only because background delivery is enabled below
        }
    }
    store.execute(query)

    store.enableBackgroundDelivery(for: stepType, frequency: .hourly) { success, error in
        // success is false and error is set if registration failed
    }
}

Call this from application(_:didFinishLaunchingWithOptions:), as Apple instructs, so the query exists before a delivery arrives.

Turning it off#

Three calls undo the setup. stop(_:) ends the query itself. disableBackgroundDelivery(for:withCompletion:) "Disables background deliveries of update notifications for the specified data type", and disableAllBackgroundDelivery(completion:) "Disables all background deliveries of update notifications." Both have async throws forms, and both call their completion handler on a background queue.

Pitfalls#

  • Looking for the data in the handler. It is not there. Run a second query, preferably anchored, and call the completion handler when that finishes rather than straight away.
  • Calling the completion handler when you have not enabled background delivery. Apple says to call it only when you are using background updates.
  • Treating the frequency as a timetable. .hourly means at most once an hour, and only after a change. No change, no wake.
  • Registering a correlation type. Apple says HKCorrelationType is not supported for background delivery. Register the underlying quantity types instead.
  • Creating the query lazily. If the query is built only when a screen opens, a background launch finds no query to call. Apple's place for it is app launch.
  • Testing in the Simulator. Apple's class page: "Background server queries aren't supported on the Simulator. Be sure to test your background queries on a device." The testing side is covered in testing background delivery.

Frequently asked questions

Does the HKObserverQuery update handler tell me which samples changed?
No. Apple's article states that the observer query's update handler receives no information about the change, only that a change occurred, and that you must execute another query, such as an HKSampleQuery or HKAnchoredObjectQuery, to access the changes. The multi-type initializer adds a set of the sample types that changed, which narrows what to re-read but still carries no samples.
Should I call the HKObserverQuery completion handler if background delivery is not enabled?
No. Apple's documentation for the update handler says you must call the completion handler as soon as you finish processing when you have registered for background updates, and that you should only call it when you are using background updates. In a foreground-only observer query, leave it uncalled.
Does HKUpdateFrequency.hourly mean HealthKit wakes my app every hour?
No. Apple defines hourly as the system launching your app at most once an hour in response to changes, so it is a ceiling that applies only when matching data has changed. Apple also notes that some sample types have a maximum frequency of hourly, which the system enforces transparently whatever frequency you request.
How do I stop HealthKit background delivery for one data type?
Call disableBackgroundDelivery(for:withCompletion:) on HKHealthStore with that type, or its async throws form. Apple documents that it prevents your app from receiving further background update notifications for the type and calls its completion handler on a background queue once delivery is disabled. disableAllBackgroundDelivery(completion:) does the same for every type, and stop(_:) ends the observer query itself.
Can one HKObserverQuery watch more than one sample type?
Yes, from iOS 15.0 and watchOS 8.0. The initializer init(queryDescriptors:updateHandler:) takes an array of HKQueryDescriptor objects, and Apple says each can specify a different data type. Its update handler adds a parameter listing the sample types of all the changed objects, so you can re-read only the types that moved.

All 40 questions in HealthKit Queries →

Keep reading

Elsewhere on the site

Pages that share this one’s concepts and sources, from other sections.

Next steps

Was this page useful?

Independent comparison, last reviewed October 3, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.

← All healthkit queries · by AIFitnessAPI