HealthKit Background Delivery Not Working: Why Your Observer Never Fires
Last verified August 12, 2026 · 8 min read
Covered here:HealthKit
The code looks right. There is an HKObserverQuery, there is a call to enableBackgroundDelivery, the app reads steps perfectly when it is open, and the update handler has never once run while the phone was in a pocket. Almost every instance of this is one of a small set of documented gates, and four of them are stated in a single paragraph of Apple's reference page that most people skim past on the way to the code sample.
Work them in order. Each one is cheap to check and each one, on its own, is sufficient to produce total silence.
Gate 1: the entitlement you probably do not have#
Apple's requirement is unambiguous. For iOS 15 and watchOS 8 and later, you must enable HealthKit Background Delivery by adding the com.apple.developer.healthkit.background-delivery entitlement to your app, and if your app does not have it, enableBackgroundDelivery(for:frequency:withCompletion:) fails with an HKError.Code.errorAuthorizationDenied error.
Two things make this the number-one cause. First, the entitlement is documented as a Boolean whose default value is false — it is available from iOS 15, iPadOS 15, watchOS 8 and visionOS 1, but you get nothing unless you add the key deliberately. Second, the error is easy to miss, because Apple hands it to you through a completion block whose arguments almost everybody throws away:
healthStore.enableBackgroundDelivery(for: stepType, frequency: .hourly) { success, error in
// Both of these are load-bearing. Log them.
if let error {
// errorAuthorizationDenied here means the ENTITLEMENT is missing,
// not that the user denied anything.
log.error("background delivery not enabled: \(error)")
return
}
log.info("background delivery enabled: \(success)")
}
Note the wording trap in the error name. errorAuthorizationDenied reads like a permission refusal, and permission refusal in HealthKit is a topic with its own long list of surprises — covered in HealthKit authorization denied. Here it means nothing of the kind. The user is not involved.
Gate 2: the Simulator will never do this#
Apple prints the same sentence twice, once on the enableBackgroundDelivery reference and again on the HKObserverQuery reference: background server queries are not supported on the Simulator, and you should be sure to test your background queries on a device.
There is no flag and no partial credit. If your only evidence that background delivery does not work is a Simulator session, you have no evidence at all. This also means a Simulator-based CI pipeline covers none of this path, which is the honest boundary drawn in testing a HealthKit integration. Move to real hardware before you change a line.
Gate 3: the query does not exist when the wake arrives#
This one produces the most confusing symptom, because everything looks correct in the foreground.
Apple documents the launch sequence directly: as soon as your app launches, HealthKit calls the update handler for any observer queries that match the newly saved data, and if you plan on supporting background delivery, you should set up all your observer queries in your app delegate's application(_:didFinishLaunchingWithOptions:) method. Apple's own explanation for why is the part to internalise — registering there ensures the queries are instantiated and ready to use before HealthKit delivers the updates.
A background wake launches your process. If your observer is created inside a view controller's setup, or behind a feature flag that resolves after a network call, or on first navigation to a screen, then at the moment HealthKit tries to deliver there is no matching query in the process and the delivery goes nowhere. In the foreground you always happen to have visited that screen, so it always works.
Gate 4: three missed acknowledgements and you are switched off#
Apple's completion-handler documentation contains the single most consequential sentence in this whole area. You must call the block as soon as you are done processing the incoming data; if you do not, HealthKit continues to attempt to launch your app using a backoff algorithm, and if your app fails to respond three times, HealthKit assumes your app cannot receive data and stops sending you background updates.
Read that as a strike count, not as a retry policy. The handler is the heartbeat that keeps the channel alive, and the place people forget it is the error path — a thrown error, an early return on a nil unwrap, an upload that times out before the line is reached. Three of those and the install is done receiving background deliveries, which is exactly the "it worked last week" report you will get from QA.
let observer = HKObserverQuery(sampleType: stepType, predicate: nil) { _, completionHandler, error in
// Acknowledge on EVERY path, including the failure branch.
defer { completionHandler() }
if error != nil { return }
// Persist locally first, then acknowledge, then upload asynchronously.
ingestFromPersistedAnchor()
}
healthStore.execute(observer)
Symptom to cause#
| Symptom | What it means | What to do |
|---|---|---|
enableBackgroundDelivery reports an error you never logged | Missing entitlement, surfacing as errorAuthorizationDenied | Add the entitlement, set it true, re-sign |
| Nothing fires, ever, on a Simulator | Unsupported by design | Test on a device |
| Works in the foreground, silent in the background | Observer registered too late in the launch path | Register in the app delegate's launch method |
| Fired a few times, then stopped permanently | Three unacknowledged deliveries | Call the completion handler in a defer |
| Fires, but you see no new samples | The observer carries no payload | Run an anchored object query from inside the handler |
| Fires far less often than requested | frequency is a documented ceiling | Design for eventual delivery, not a cadence |
Requested .immediate for steps on iOS, still hourly | Hourly maximum, enforced transparently | Expect hourly at best for that type |
| Wake happens but the read fails or is empty | Store encrypted while the device is locked | Retry after unlock; still acknowledge |
| A correlation type is silently never delivered | HKCorrelationType is unsupported here | Register the underlying quantity types |
The four things that look like failures and are not#
The observer is a doorbell, not a parcel. Apple is explicit that the update handler does not receive any information about the change, just that a change occurred, and that you must execute another query — an HKSampleQuery or an HKAnchoredObjectQuery — to access the changes. An anchored object query is usually the right second query, because Apple describes it as combining a snapshot of what is currently stored with a long-running query that responds to updates, returning an anchor corresponding to the last sample or deleted object it saw so subsequent runs return only newer objects.
frequency is a maximum, not a schedule. Apple defines it as the maximum frequency of the updates, waking your app from the background at most once per time period specified. HKUpdateFrequency offers immediate, hourly, daily and weekly, described respectively as launching your app every time a change is detected, at most once an hour, at most once a day, and at most once per week. Nothing in that documentation promises a minimum rate.
Some types are capped no matter what you ask for. Apple states that some sample types have a maximum frequency of hourly and that the system enforces this frequency transparently, giving step count on iOS as the example. In watchOS most data types are hourly-capped too, with a named exception list that can reach immediate — high heart rate, low heart rate and irregular heart rhythm events, environmental and headphone audio exposure events, low cardio fitness events, number of times fallen, VO2 max, handwashing and toothbrushing events. On watchOS there is also a budget: background updates share an allowance with WKApplicationRefreshBackgroundTask, four an hour, conditioned on the app having a complication on the active watch face.
A locked phone can turn a successful wake into an empty read. Apple documents that the device encrypts the HealthKit store when the user locks it, so your app may not be able to read data from the store when it runs in the background. Writes still work and are cached until unlock. So a wake that produces nothing is not proof the user did nothing — and if your query comes back empty in the foreground too, that is a different investigation entirely, laid out in HealthKit returning no data.
After the gates#
Once delivery is genuinely working, the remaining problem is that no amount of correct code makes a wake happen. Apple publishes a ceiling and a shutdown rule; it publishes no minimum rate, no latency figure and no delivery guarantee. That means a pipeline whose correctness depends on being woken is a pipeline that will eventually be wrong. Pair every wake with a foreground reconciliation and a server-side staleness check, so a missed delivery costs latency rather than a wrong number — the design is worked through in background sync that does not depend on the phone waking up.
Frequently asked questions
- Why does enableBackgroundDelivery fail with errorAuthorizationDenied when the user already granted access?
- Because that error is about your entitlement, not the person's choice. Apple documents that for iOS 15 and watchOS 8 and later you must enable HealthKit Background Delivery by adding the com.apple.developer.healthkit.background-delivery entitlement to your app, and that if your app does not have it, the method fails with an HKError.Code.errorAuthorizationDenied error. The entitlement is a Boolean whose default value is false, so it has to be added explicitly and present in the signed build you are testing. The fastest way to see this is to stop discarding the completion block's error argument.
- My observer query fires but finds nothing new. What is wrong?
- Probably nothing, because an observer query is not designed to carry data. Apple states that the update handler does not receive any information about the change, just that a change occurred, and that you must execute another query, for example an HKSampleQuery or HKAnchoredObjectQuery, to access the changes. If you are already running a second query, the other documented explanation is a locked device: Apple notes the system encrypts the HealthKit store when the user locks the device, so your app may not be able to read from the store while it runs in the background, and our architecture guide records that surfacing as errorDatabaseInaccessible. Treat that as retryable rather than as an absence of data.
- Which HealthKit types can be registered for background delivery?
- Apple documents the type parameter as accepting an HKCharacteristicType, HKQuantityType, HKCategoryType, or HKWorkoutType, and states outright that HKCorrelationType is not a supported type for background delivery. That asymmetry catches people, because the matching disableBackgroundDelivery method does list HKCorrelationType among the classes it accepts. If you are trying to observe a correlation such as a blood pressure reading, register the underlying quantity types instead.
- Why does my observer query never fire in the iOS Simulator?
- Because Apple does not support it there. The sentence appears on both the enableBackgroundDelivery reference and the HKObserverQuery reference: background server queries are not supported on the Simulator, and you should be sure to test your background queries on a device. There is no flag or workaround, which means Simulator-based CI cannot cover this path at all and a green Simulator run is not evidence your background delivery works. Move the test to real hardware before changing any code.
- How many background updates can a watchOS app receive in an hour?
- Apple documents that in watchOS, background updates share a budget with WKApplicationRefreshBackgroundTask tasks, and that your app can receive four updates or background app refresh tasks an hour as long as it has a complication on the active watch face. Read the whole sentence, because the complication is the condition on the budget rather than a suggestion. Apple also documents that in watchOS most data types have an hourly maximum frequency, with a named exception list that can reach immediate, including high heart rate, low heart rate, and irregular heart rhythm events, VO2 max, and number of times fallen.
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 August 12, 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 troubleshooting · by AIFitnessAPI