---
title: "HealthKit Background Delivery Not Working: Why Your Observer Never Fires"
canonical: "https://aifitnessapi.com/fix/healthkit-background-delivery-not-working"
cluster: "Troubleshooting"
primary_query: "healthkit background delivery not working"
last_reviewed: "2026-08-12"
description: "Your HKObserverQuery never wakes? Check the entitlement, where the query is registered, the completion handler, and Apple's documented frequency ceiling."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"HealthKit Background Delivery Not Working: Why Your Observer Never Fires\", AIFitnessAPI, https://aifitnessapi.com/fix/healthkit-background-delivery-not-working"
---

# HealthKit Background Delivery Not Working: Why Your Observer Never Fires

> If your HKObserverQuery never fires while your app is backgrounded, work down four documented gates before you suspect a bug. Since iOS 15 and watchOS 8 you must add the com.apple.developer.healthkit.background-delivery entitlement, which defaults to false; without it Apple documents that enableBackgroundDelivery(for:frequency:withCompletion:) fails with an HKError.Code.errorAuthorizationDenied error. Apple states on two separate pages that background server queries are not supported on the Simulator, so a Simulator test proves nothing. Observer queries must be set up in the app delegate's application(_:didFinishLaunchingWithOptions:) method so they exist before HealthKit delivers to a freshly launched process. And you must call the update's completion handler: if your app fails to respond three times, Apple documents that HealthKit assumes it cannot receive data and stops sending background updates.

- Canonical: https://aifitnessapi.com/fix/healthkit-background-delivery-not-working
- Last reviewed: 2026-08-12
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "HealthKit Background Delivery Not Working: Why Your Observer Never Fires", AIFitnessAPI, https://aifitnessapi.com/fix/healthkit-background-delivery-not-working

---

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:

```swift
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](/fix/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](/test/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.

```swift
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](/fix/healthkit-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](/architecture/background-sync).

## FAQ

### 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.

[Permalink](https://aifitnessapi.com/fix/healthkit-background-delivery-not-working#faq-1)

### 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.

[Permalink](https://aifitnessapi.com/fix/healthkit-background-delivery-not-working#faq-2)

### 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.

[Permalink](https://aifitnessapi.com/fix/healthkit-background-delivery-not-working#faq-3)

### 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.

[Permalink](https://aifitnessapi.com/fix/healthkit-background-delivery-not-working#faq-4)

### 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.

[Permalink](https://aifitnessapi.com/fix/healthkit-background-delivery-not-working#faq-5)
