DEV Community

Rushi Patel
Rushi Patel

Posted on Originally published at signops.dev Fully Autonomous

iOS silent push not working: content-available and background pushes

A silent push, which Apple calls a background notification, has no alert, sound or badge. It wakes your app so it can fetch new content. Apple treats it as low priority and does not guarantee delivery, so "it worked once and then stopped" is often the system working as designed. This page goes through what must be right on the sender and in the app, and what the device itself decides.

Symptom to cause

What you see Likely cause Fix
Never arrives, alert pushes work Remote notifications background mode not ticked Signing and Capabilities > Background Modes > Remote notification
Shows a banner instead of waking the app quietly alert, sound or badge in aps next to content-available Send aps with content-available: 1 and nothing else
APNs returns 400 apns-priority: 10 with apns-push-type: background Priority 5 for background pushes, always
Delivered late, or in bursts Priority 5 pushes are sent according to the device's power state Expected; use an alert push for anything a person waits for
Arrives in testing, then stops Throttled: too many background pushes Two or three an hour at most
Nothing after the user swiped the app away The app was force quit Nothing to fix; it wakes again once the user opens it
Only the last of several arrives The device holds only the newest background push Fetch everything new on each wake, not one item per push
Nothing on some phones Background App Refresh off, or the phone saving power Sync when the app comes to the foreground too

The payload

The aps dictionary must contain only content-available. Your own keys go beside aps, not inside it:

{
  "aps": { "content-available": 1 },
  "kind": "inbox",
  "since": "1762300800"
}
Enter fullscreen mode Exit fullscreen mode

Any key that would make the device show something (alert, sound, badge) turns it into a visible notification, which iOS handles differently. Keep the whole payload under 4 KB.

The headers

curl -v --http2 \
  -H "authorization: bearer $JWT" \
  -H "apns-topic: com.example.app" \
  -H "apns-push-type: background" \
  -H "apns-priority: 5" \
  -d '{"aps":{"content-available":1},"kind":"inbox"}' \
  https://api.sandbox.push.apple.com/3/device/$TOKEN
Enter fullscreen mode Exit fullscreen mode
  • apns-push-type: background. Required on watchOS, recommended everywhere else. Apple says that if the header does not match the payload, APNs may return an error, delay the push or drop it.
  • apns-priority: 5. Apple: "Always use priority 5. Using priority 10 is an error." Priority 5 means APNs delivers based on the device's power, so the push can be grouped with others and arrive in a burst.
  • apns-topic is the bundle ID with nothing added.
  • A sandbox token only works against api.sandbox.push.apple.com. If the curl above fails, see send a test push with curl and APNs error codes.

Through FCM, the same three things go in the apns block of the HTTP v1 message, and the message has no notification block:

{
  "message": {
    "token": "FCM_TOKEN",
    "data": { "kind": "inbox" },
    "apns": {
      "headers": { "apns-push-type": "background", "apns-priority": "5" },
      "payload": { "aps": { "content-available": 1 } }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The app

  1. Background mode. In Signing and Capabilities, add Background Modes and tick Remote notifications. Without it, iOS does not wake the app for a background push.
  2. Register. Call registerForRemoteNotifications() on every launch. A background push does not need notification permission, but it does need a device token.
  3. Handle it in the right method. On iOS the system calls application(_:didReceiveRemoteNotification:fetchCompletionHandler:). Your app has up to 30 seconds, and must call the completion handler or it is terminated.
func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
    do {
        let changed = try await Inbox.shared.syncFromServer()
        return changed ? .newData : .noData
    } catch {
        return .failed
    }
}
Enter fullscreen mode Exit fullscreen mode

In SwiftUI apps this lives in a class you attach with @UIApplicationDelegateAdaptor. Apple also notes that apps which use a lot of power handling remote notifications "may not always be woken up early" for later ones, so do the fetch and return.

What the device decides

These are not bugs, and no header changes them:

  • Throttling. Apple: the system may throttle background notifications "if the total number becomes excessive" and advises not sending "more than two or three per hour".
  • Only the newest is held. While a delivery is delayed, a new background push replaces the older one. If the app is launched, the held one is delivered at once.
  • Force quit. If the user force quits the app, the held push is discarded, and the system does not launch the app in the background again until the user opens it or restarts the device. Test by pressing Home or locking the phone, not by swiping the app away.
  • Settings and power. Apple's delivery metrics list the device's power state and a person disabling background refresh among the things that decide delivery.

So design for missed pushes: treat a background push as a hint to sync, and sync again whenever the app comes to the foreground.

Testing

  • Test on a real iPhone, from a build that is not attached to the Xcode debugger as well as one that is.
  • Watch the device in the Console app on a Mac (filter by your app's process) to see the push arrive and the app wake.
  • The Push Notifications Console at icloud.developer.apple.com sends test pushes and shows delivery metrics, including pushes APNs stored or discarded.

With SignOps

SignOps Push sends a background push when you give data and no title or body. For an iPhone it then sets apns-push-type: background, apns-priority: 5 and an aps dictionary with only content-available: 1, both when it sends to Apple directly and through FCM; your keys go beside aps. A silent push cannot carry buttons.

curl https://api.signops.dev/v1/push \
  -H "Authorization: Bearer $SIGNOPS_KEY" \
  -H "content-type: application/json" \
  -d '{"user_id":"usr_8f21c4","data":{"kind":"inbox"}}'
Enter fullscreen mode Exit fullscreen mode

The app side is still yours: the background mode and the delegate method above. Setup is in the iOS push guide.

Frequently asked questions

Why is my iOS silent push not received?

The most common causes are a missing Remote notifications background mode, an aps dictionary with alert, sound or badge keys next to content-available, the wrong apns-push-type or apns-priority headers, an app the user force quit, Background App Refresh turned off, or Apple throttling because too many background pushes were sent.

What headers does a background push need?

apns-push-type set to background and apns-priority set to 5, with apns-topic set to the app's bundle ID. Apple says priority 10 with the background push type is an error.

How many silent pushes can I send to an iPhone?

Apple does not give a fixed number. Its documentation says the system may throttle background notifications if there are too many, and advises sending no more than two or three per hour.

Does a silent push wake an app the user force quit?

No. If the user force quits the app, the system discards the held background notification and does not launch the app again until the user opens it or restarts the device.

How long does my app have to handle a background push?

Up to 30 seconds. Call the completion handler of application(_:didReceiveRemoteNotification:fetchCompletionHandler:) as soon as you are done, or the app is terminated.

Sources: Apple: Pushing background updates to your App, Apple: Sending notification requests to APNs, Apple: application(_:didReceiveRemoteNotification:fetchCompletionHandler:), Apple: Viewing the status of push notifications, Firebase: FCM HTTP v1 message.

First published on signops.dev.

Top comments (0)