---
term: 'Real-Time Push Notifications vs. Live Query Subscriptions'
seoTitle: 'Push Notifications vs. Live Queries: Which Real-Time Channel?'
headline: 'Push Notifications vs. Live Query Subscriptions: which do you need?'
slug: push-notifications-vs-live-queries
category: api-realtime
shortDefinition: 'A push notification is an OS-delivered alert that reaches closed apps; a live query streams data changes while the app is open.'
relatedTerms:
  - push-notifications-apns-fcm
  - real-time-live-queries
  - websockets-real-time-sync
  - sse-vs-websockets-vs-polling
contrastsWith:
  - real-time-live-queries
aboutTerms:
  - 'Push Notifications'
  - 'Live Query Subscriptions'
faq:
  - question: 'What is the difference between push notifications and live queries?'
    answer: 'The delivery path and the app state it serves. A push notification travels through the operating system''s push gateway and reaches the device even when your app is closed — but carries a small, opaque payload and best-effort guarantees. A live query is a WebSocket subscription your running app holds open: full data objects, real-time, permission-checked — and dead the moment the app is.'
  - question: 'Do live queries work when the app is closed?'
    answer: 'No, and no client-side cleverness changes that. A live query is state inside your running process — a WebSocket the app holds open. When the OS suspends or kills the app, the socket dies with it, and mobile platforms aggressively suspend backgrounded apps to save battery. Reaching a closed app is exactly the job the OS reserves for its own push gateway.'
  - question: 'Should a chat app use push notifications or live queries?'
    answer: 'Both, split by app state. The open conversation subscribes to a live query — messages render instantly with full data, typing and read receipts included. The closed app relies on push to alert the recipient, carrying just enough payload to render the banner. The tap opens the app, which re-subscribes and re-queries to catch up. Every mainstream messenger works this way.'
  - question: 'Are push notifications guaranteed to arrive?'
    answer: 'No — delivery is best-effort by design. The OS gateways coalesce, throttle, and drop messages under battery pressure, users disable permissions entirely, and devices go dark. Silent background pushes are throttled even harder than visible ones. Treat push as a wake-up tap on the shoulder, never as a data-transport contract; the app must reconcile state by querying after it opens.'
  - question: 'How big can a push notification payload be?'
    answer: 'Kilobytes, not data. The OS gateways cap payloads at roughly 4 KB, and the payload is opaque to your backend''s permission model — whatever you put in it sits in the notification pipeline outside your ACLs. The robust pattern sends identifiers and display strings only, and lets the opened app fetch the real objects through the normal, permission-checked API.'
  - question: 'What is a silent push notification?'
    answer: 'A push with no visible alert that asks the OS to wake your app briefly in the background — typically to prefetch data so the next open feels instant. It is the most rationed resource in the push system: the OS budgets wakeups per app per day and ignores excess, so silent push works as an optimization layer, never as a reliable sync channel.'
  - question: 'Do I need both push notifications and live queries?'
    answer: 'If users care about events that happen while the app is closed — messages, orders, alerts — yes, almost unavoidably. The two channels cover disjoint app states: live queries own the open-app experience, push owns re-engagement from the closed state. Backends with both built in let one database write fan out to each channel, so needing both stops implying building twice.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'RFC 8030 — Generic Event Delivery Using HTTP Push'
    url: 'https://datatracker.ietf.org/doc/html/rfc8030'
  - name: 'Push API — MDN Web Docs'
    url: 'https://developer.mozilla.org/en-US/docs/Web/API/Push_API'
  - name: 'Push technology (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Push_technology'
  - name: 'Live Queries — Parse Server guide'
    url: 'https://docs.parseplatform.org/parse-server/guide/#live-queries'
cta:
  title: 'One backend, both channels'
  text: 'Back4app ships push notifications and Live Queries on the same data: one database write fans out to the OS push gateways and to every subscribed WebSocket. Wire the afterSave trigger once and cover every app state.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-08-05'
translationKey: push-notifications-vs-live-queries
---

**A push notification is an OS-delivered alert that reaches closed apps; a live query streams data changes while the app is open.** Framing them as rivals is the classic mistake — they cover *disjoint app states*, and the real design question is not "which one" but "where is the user right now?" Most apps that feel properly real-time run both and route by that answer.

## Key takeaways

| Question | Answer |
| --- | --- |
| Push notifications | OS gateway delivery (APNs, FCM) — reaches closed apps, ~4 KB opaque payload, best-effort |
| Live queries | WebSocket subscription — full permission-checked objects, real time, app must be open |
| The rivalry | False — they serve disjoint app states and compose, not compete |
| The routing rule | App open → live query · app closed → push · tap on push → open, re-subscribe, catch up |
| The failure mode | Using push as a data channel, or expecting sockets to outlive the process |

## Both channels, in code

The pattern nearly every messaging, ordering, and alerting app converges on:

**JavaScript:**

```javascript
// JavaScript — Back4app JS SDK: one channel per app state
// While the app is OPEN — the live query delivers the data itself
const messages = new Parse.Query('Message');
messages.equalTo('conversation', conversationId);
const sub = await messages.subscribe();
sub.on('create', (m) => appendBubble(m));   // full object, real time

// While it is CLOSED — push owns delivery: register this device
const installation = await Parse.Installation.currentInstallation();
installation.set('channels', [`user-${currentUser.id}`]);
await installation.save();
// A Cloud Code afterSave trigger sends the push to this channel —
// the OS shows the alert; the tap opens the app, which re-subscribes.
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK: one channel per app state
// While the app is OPEN — the live query delivers the data itself
final liveQuery = LiveQuery();
final messages = QueryBuilder<ParseObject>(ParseObject('Message'))
  ..whereEqualTo('conversation', conversationId);

final sub = await liveQuery.client.subscribe(messages);
sub.on(LiveQueryEvent.create, (m) => appendBubble(m)); // full object

// While it is CLOSED — push owns delivery: register this device
final installation = await ParseInstallation.currentInstallation();
installation.set('channels', ['user-$userId']);
await installation.save();
// A Cloud Code afterSave trigger sends the push to this channel —
// the OS shows the alert; the tap opens the app, which re-subscribes.
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK: one channel per app state
// While the app is OPEN — the live query delivers the data itself
let messages = Message.query("conversation" == conversationId)
let subscription = messages.subscribeCallback
subscription?.handleEvent { _, event in
  if case .created(let m) = event { appendBubble(m) }  // full object
}

// While it is CLOSED — push owns delivery: register this device
var installation = Installation.current
installation?.channels = ["user-\(userId)"]
installation?.save { _ in }
// A Cloud Code afterSave trigger sends the push to this channel —
// the OS shows the alert; the tap opens the app, which re-subscribes.
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK: one channel per app state
// While the app is OPEN — the live query delivers the data itself
val client = ParseLiveQueryClient.Factory.getClient()
val messages = ParseQuery.getQuery<ParseObject>("Message")
messages.whereEqualTo("conversation", conversationId)

val sub = client.subscribe(messages)
sub.handleEvent(SubscriptionHandling.Event.CREATE) { _, m ->
    appendBubble(m)                                  // full object
}

// While it is CLOSED — push owns delivery: register this device
val installation = ParseInstallation.getCurrentInstallation()
installation.put("channels", listOf("user-$userId"))
installation.saveInBackground()
// A Cloud Code afterSave trigger sends the push to this channel.
```

Note what each half handles: the subscription delivers *data* — whole objects, typed events, in real time. The installation registers for *attention* — the right to interrupt the user later, through a pipeline your app does not control.

## Two deliveries, two owners

The paths could hardly be more different, and every property in the comparison table falls out of who owns the last mile.

A **push notification** leaves your backend as a request to an OS-operated gateway — the iOS and Android push gateways (APNs, FCM), or a browser push service speaking [RFC 8030](https://datatracker.ietf.org/doc/html/rfc8030) on the web. The gateway owns delivery: it holds messages for offline devices, coalesces and throttles under battery pressure, and wakes your app or renders the banner. That borrowed power is the whole point — *only* the OS can reach a process that isn't running — and the whole constraint: payloads capped around 4 KB, delivery best-effort, silent background wakeups rationed per day, and the payload outside your backend's permission model.

A **live query** never leaves your trust boundary: the app holds a [WebSocket](/glossary/websockets-real-time-sync/) to the backend's subscription server, which matches every database write against the subscribed query and pushes typed events — create, update, enter, leave, delete — with [ACLs enforced per subscriber](/glossary/real-time-live-queries/). Full objects, real-time latency, no payload ceiling worth naming. The dependency is brutal in exchange: the subscription is state inside your process, and when the OS suspends the app — which mobile platforms do aggressively — the socket, and the channel, are gone.

```mermaid
flowchart TB
  accTitle: Routing real-time delivery by app state
  accDescr: A database write triggers backend logic. If the recipient's app is open, a live query pushes the full object over a WebSocket. If the app is closed, the backend sends a small payload through the OS push gateway, which displays a notification; tapping it opens the app, which re-subscribes and catches up by querying.
  W["Database write<br/>(message, order, alert)"] --> T["Backend trigger<br/>(afterSave)"]
  T -->|"app open"| LQ["Live query push<br/>full object · WebSocket"]
  T -->|"app closed"| GW["OS push gateway<br/>(APNs, FCM)"]
  GW --> N["Notification banner<br/>~4 KB payload"]
  N -->|"tap"| O["App opens →<br/>re-subscribe + catch-up query"]
  LQ --> UI["Screen updates in place"]
  O --> UI
```

## Push notifications vs. live queries

| | Push notifications | Live query subscriptions |
| --- | --- | --- |
| Reaches a closed app | **Yes — the defining power** | No — subscription dies with the process |
| Payload | ~4 KB, opaque to your ACLs | Full objects, permission-checked per subscriber |
| Delivery guarantee | Best-effort; coalesced, throttled, droppable | Reliable while connected; catch-up needed after gaps |
| Latency | Seconds-ish, gateway-dependent | Real time (~RTT) |
| Transport owner | The OS and its gateway | Your backend's WebSocket fleet |
| User consent | Permission prompt; user can revoke | None needed — it's just your app's data |
| Cost of misuse | Notification fatigue, uninstalls | Battery and socket load if over-subscribed |
| Built for | Attention and re-engagement | Data and in-app state |

The rows compose cleanly because the two channels answer different questions: push answers *"how do I reach the user?"*, live queries answer *"how does the screen stay true?"* — which is why the [transport-level comparison](/glossary/sse-vs-websockets-vs-polling/) of SSE, WebSockets, and polling lives entirely inside the second question.

## The handoff: where apps actually break

The bugs live at the seam between channels. A user taps a push about a message that was *also* delivered by live query before the app suspended — deduplicate by object ID, not by channel. A push arrives about data the user can no longer access — fetch through the normal API on open and let the ACLs answer, never trust the payload. The app was closed for three days — the reopened app cannot replay a gap from push (notifications are not a journal), so the handoff is always *re-subscribe, then re-run the base query* to rebuild truth, with the [push token registration](/glossary/push-notifications-apns-fcm/) kept fresh in the background. Design the seam once and both channels become boring — which is the goal.

## Common use cases

- **Chat and messaging** — live query renders the open conversation; push carries "new message" through the closed state. The canonical both-channels app.
- **Order and delivery tracking** — the open tracking screen subscribes; status flips to "delivered" while closed arrive as push.
- **Operational alerting** — on-call consoles subscribe for the wallboard; the pager path is push, because nobody keeps the app open at 3 a.m.
- **Auctions and drops** — live price movement in-app; outbid-while-away is a push, deep-linking back into the live screen.
- **Social engagement** — likes and replies land as push re-engagement; the opened feed goes live via subscription.

## Which channel should you use? A decision matrix

| Your situation | Reach for |
| --- | --- |
| Screen is open and must stay current | Live query |
| Event happens while the app is closed and the user should know | Push notification |
| Payload is sensitive or permission-gated | Live query — or push identifiers only, fetch on open |
| You need guaranteed, ordered delivery of data | Neither alone — query-based catch-up on open, channels as accelerators |
| Updating an in-app badge or count in real time | Live query |
| Re-engaging users who have not opened the app in days | Push — it is the only channel that can |
| Kiosk or wallboard that never sleeps | Live query only; push adds nothing |

## Limitations and trade-offs

- **Push is not a data channel.** Payload caps, opaque routing outside your ACLs, and coalescing make it structurally wrong for carrying state — send pointers, fetch truth on open.
- **Push delivery is a probability, not a promise.** Battery optimizers, revoked permissions, and gateway throttling all drop messages silently; anything that must not be missed needs a query-based reconciliation path.
- **Live queries stop at the process boundary.** No socket survives suspension; treating a subscription as an always-on channel is the false assumption behind most "we missed events" bugs.
- **Both channels tax the client.** Over-broad subscriptions burn battery and server matching; over-eager push burns goodwill — the uninstall is the user's rate limiter.
- **The seam is your responsibility.** Deduplication, catch-up queries, and token refresh are application logic; neither channel provides them for free.

## Push and live queries on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Both channels ship built in and share one write path: a Cloud Code `afterSave` trigger on the same database write can send the push — through the platform's gateway integrations with device and channel targeting — while [Live Query](/glossary/real-time-live-queries/) fans the full object out to every subscribed, permission-checked client automatically. The routing diagram above collapses to one trigger and one subscribe call, and the seam logic — catch-up queries, token registration via the Installation class — runs through the same SDKs shown in the code tabs.
