DEV Community

Alok Singh
Alok Singh

Posted on

iOS Silent Push Notifications That Actually Arrive: content-available, Priority 5 and the Throttling Rules

Silent push notifications (Apple calls them background notifications) are the push you send when you don't want to show anything. You just want the app to wake up, fetch new data, refresh a cache or sync a record. They're also the push type developers most often swear "doesn't work."

Usually it does work. It just follows rules that visible alerts don't. This guide covers the exact payload and headers, the app-side code, and the reasons a silent push gets delayed or dropped.

1. The payload

A silent push is a payload with content-available set to 1 and no alert, sound or badge:

{
  "aps": {
    "content-available": 1
  },
  "type": "sync",
  "since": "2026-11-09T04:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Anything outside aps is your own data and is handed to the app. Keep the total under 4 KB.

If you add an alert to the same payload, it's no longer a silent push. It becomes a visible notification that also happens to wake the app, and it follows the visible-alert rules instead.

2. The headers (this is where most failures come from)

When you call APNs over HTTP/2, a background notification needs these headers:

Header Value
apns-push-type background
apns-priority 5
apns-topic your app's bundle ID

Two rules matter here:

  • apns-push-type must be background. Since iOS 13 the header is required, and if it says alert for a payload with no alert, delivery isn't reliable.
  • Priority must be 5, not 10. Priority 10 means "deliver immediately and show something to the user." Apple documents that background notifications sent with priority 10 can be rejected or not delivered.

A quick test with curl once you have a provider token ($JWT) and a device token:

curl -v --http2 \
  --header "authorization: bearer $JWT" \
  --header "apns-topic: com.example.myapp" \
  --header "apns-push-type: background" \
  --header "apns-priority: 5" \
  --data '{"aps":{"content-available":1},"type":"sync"}' \
  "https://api.sandbox.push.apple.com/3/device/$DEVICE_TOKEN"
Enter fullscreen mode Exit fullscreen mode

A 200 response with an apns-id header means APNs accepted it. Accepted doesn't mean delivered, which is the next section.

Use api.sandbox.push.apple.com for builds you run from Xcode and api.push.apple.com for TestFlight and App Store builds. A token from one environment won't work in the other.

3. The app side

First, turn on the capability. In Xcode, open Signing & Capabilities, add Background Modes, and tick Remote notifications. Without it, the app is never woken.

Register for remote notifications. Note that a silent push doesn't need the user's permission to show alerts, so you can register even if they declined:

import UIKit

final class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        application.registerForRemoteNotifications()
        return true
    }

    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%02x", $0) }.joined()
        print("APNs device token:", token)
        // send it to your server
    }

    func application(_ application: UIApplication,
                     didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                     fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        guard userInfo["type"] as? String == "sync" else {
            completionHandler(.noData)
            return
        }
        Task {
            do {
                let changed = try await SyncService.shared.pullChanges()
                completionHandler(changed ? .newData : .noData)
            } catch {
                completionHandler(.failed)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In a SwiftUI app, hook the delegate in with @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate.

Two things to get right in that handler:

  • Always call the completion handler, on every path. You get roughly 30 seconds of background time. Not calling it, or running too long, teaches the system that your app is expensive to wake.
  • Return an honest result. Use .newData, .noData or .failed to reflect what actually happened.

4. Why silent pushes don't arrive

Background notifications are low priority by design, and iOS decides when to deliver them. These are the common reasons yours seem lost:

  1. The user force-quit the app. If someone swipes the app away in the app switcher, iOS won't launch it for background notifications until they open it again. This is the number one "it worked yesterday" cause.
  2. Throttling. Apple's guidance is not to send more than two or three per hour. Send more and the system will hold or coalesce them. Only the latest one may be delivered.
  3. Low Power Mode and battery state. With Low Power Mode on or a low battery, the system defers background work.
  4. Background App Refresh is off, either for your app or globally in Settings.
  5. Wrong headers. Priority 10, or a missing or wrong apns-push-type, as covered above.
  6. Wrong environment. A sandbox token sent to the production endpoint (or the reverse) returns 400 BadDeviceToken.

If you need delivery to be immediate and guaranteed, a silent push is the wrong tool. Send a visible alert, or use a Notification Service Extension with mutable-content to fetch data while showing a notification.

5. A debugging checklist

When a background notification isn't waking your app, go down this list:

  • Test on a physical device, from a build installed with Xcode, using the sandbox endpoint.
  • Make sure the app is in the background and not force-quit. Open it, press Home, then send.
  • Check the APNs response code and keep the apns-id.
  • Plug the phone into a Mac, open Console.app, select the device, and filter for dasd and your bundle ID. You'll see lines showing whether the system decided to launch your app, or why it deferred it.
  • Add a log or a local notification inside didReceiveRemoteNotification so you know for sure when it runs.
  • Leave a few minutes between test sends so you don't trigger throttling while debugging.

Wrapping up

A silent push needs content-available: 1, apns-push-type: background, priority 5 and the Remote notifications background mode. Then you have to accept that iOS treats it as a hint, not a command. Design your sync so a missed push is harmless, and keep a foreground refresh as the backstop.

If you'd rather send test pushes without writing a backend, I built Push Notification – Tester, a free Mac app that sends APNs pushes, including silent payloads, using your push certificate from Keychain, to Development or Production. Full disclosure: I'm its developer.

This post was written with AI assistance and reviewed by me before publishing.

Top comments (0)