FCM Is a Wake-Up Path, Not a Real-Time Transport

A mobile application can maintain a WebSocket while it is active and still need Firebase Cloud Messaging when the operating system suspends it. Those mechanisms solve different failure conditions.

WebSocket, MQTT, and SignalR assume that a client can participate in a live communication session. FCM is useful precisely when that assumption no longer holds: the application may be backgrounded, its process may not be running, or its persistent connection may have disappeared.

Treating FCM as another real-time transport hides that boundary. A more robust design uses the live channel while it exists and treats push delivery as a signal that tells the application to resume and reconcile state.

The connection is the dividing line

WebSocket establishes a bidirectional connection between two endpoints. MQTT normally maintains a client connection to a broker and adds messaging semantics such as topics, subscriptions, sessions, and Quality of Service. ASP.NET Core SignalR manages a higher-level real-time connection and can use WebSockets, Server-Sent Events, or Long Polling depending on client and server capabilities.

All three depend on an active communication path.

FCM has a different shape:

persistent channel available

client <====================> application server
       WebSocket / MQTT /
             SignalR

When that path disappears, the server cannot send another WebSocket frame through the dead connection. Keeping application state in a broker does not make a suspended process execute. SignalR reconnect logic cannot run while the operating system is not giving the application execution time.

A push service provides another route:

application server
       |
       | push request
       v
      FCM
       |
       | OS-mediated delivery
       v
mobile device
       |
       | app resumes
       v
sync authoritative state

The important architectural property is not that FCM is “slower WebSocket.” It is that delivery is mediated by the mobile platform rather than by the application’s own persistent connection.

A push message should not become the database

Suppose a chat server accepts message m42 while the recipient is offline. One design puts the complete message into an FCM payload and assumes that receiving the push completes delivery.

That couples durable application state to a notification channel.

A safer boundary stores the message first and sends a small invalidation or wake-up signal afterward:

sender
  |
  | SEND m42
  v
server
  |
  +---- persist m42
  |
  +---- FCM: "conversation 7 changed"
                  |
                  v
             recipient
                  |
                  | GET /sync?cursor=...
                  v
                server

If the push arrives, the client synchronizes. If several pushes collapse into one effective wake-up, the synchronization still reconstructs current state. If a push is delayed, the next application resume can perform the same synchronization without depending on the missing notification body.

This changes the role of FCM from state transfer to state invalidation.

Delivery acknowledgement belongs to the application

FCM acceptance is not the same event as application delivery. The same distinction exists at other layers.

MQTT QoS 1, for example, specifies at-least-once delivery for the MQTT protocol exchange and uses PUBACK. QoS 2 adds a protocol flow for exactly-once delivery between the relevant MQTT peers. Those guarantees do not prove that a chat UI rendered a message or that a person read it.

WebSocket provides framing over a live connection but does not define application-level concepts such as “stored,” “delivered to device,” or “read.” SignalR similarly gives an application a real-time programming model; product-level delivery states remain application semantics.

A messaging system therefore benefits from explicit boundaries:

message_id = m42

sender -> server       SEND m42
sender <- server       ACCEPTED m42

server -> recipient    MESSAGE m42
server <- recipient    DELIVERED m42

recipient -> server    READ m42

FCM can cause the recipient to reconnect and reach the DELIVERED transition. The push itself should not be mistaken for that transition.

FCM and WebSocket form a foreground/background pair

For a mobile chat application, a common state machine is more useful than a protocol comparison table:

              app active
                  |
                  v
          persistent channel
          WebSocket / SignalR
                  |
          connection lost
                  v
              offline
                  |
             FCM signal
                  v
          app gets execution
                  |
             reconnect
                  v
                sync
                  |
                  +------> live channel

While the application is active, the persistent connection carries low-latency events. When the application loses that connection, the server records events durably. FCM provides a path for prompting the device when platform policy permits. After execution resumes, the client fetches state using an application cursor or another synchronization mechanism.

This design does not require every FCM notification to map one-to-one to a domain event. Ten new messages can be represented by one wake-up signal because the sync operation, not the push count, determines what the client has missed.

MQTT changes messaging semantics, not mobile process policy

MQTT is attractive when the application benefits from brokered publish/subscribe, retained messages, session state, and explicit QoS levels. MQTT 5 defines QoS 0 as at most once, QoS 1 as at least once, and QoS 2 as exactly once for the protocol delivery flow.

Those features address message exchange while MQTT peers can communicate and, depending on session configuration, state that survives a disconnected network session.

They do not grant a mobile process unrestricted background execution.

This distinction matters because a developer can replace WebSocket with MQTT and still encounter the same mobile lifecycle problem:

broker
  X
  |
  | no usable client connection
  |
suspended mobile process

MQTT may improve session recovery after reconnect. FCM can help trigger the opportunity to reconnect. They are complementary when both properties are needed.

SignalR removes connection plumbing, not the offline boundary

SignalR is also easy to compare too directly with FCM. ASP.NET Core SignalR manages connections, routes messages to clients or groups, and selects among supported real-time transports. WebSockets is its preferred transport when available, with Server-Sent Events and Long Polling available as alternatives.

That abstraction saves application code from owning much of the transport negotiation and connection machinery. It does not turn an unavailable mobile process into a connected SignalR client.

A .NET backend can therefore use SignalR for active sessions and FCM for inactive mobile devices without duplicating responsibilities:

                     +--> SignalR --> connected clients
domain event --------+
                     +--> FCM -----> inactive mobile clients

Both branches should converge on the same durable application state. Otherwise reconnect behavior depends on which transport happened to carry an event.

Payload size encourages indirection

FCM messages have deliberately small payload limits. The HTTP v1 documentation specifies a maximum payload of 4096 bytes for most messages, with a lower limit for messages sent to topics in some documented contexts.

Even when a domain object fits, embedding the complete authoritative object in a push notification creates versioning and consistency problems. The server may modify or revoke the object after the notification has been queued. A client that later consumes the stale payload can reconstruct the wrong state.

An identifier or invalidation signal avoids that problem:

{
  "type": "conversation_changed",
  "conversation_id": "7",
  "cursor_hint": "18422"
}

The client treats the fields as a reason to synchronize, not as proof that local state is complete.

TTL is not an application retention policy

FCM supports a time-to-live for messages, but push TTL and application retention answer different questions.

Push TTL asks how long the push infrastructure may keep trying to deliver a notification. Application retention asks how long the server preserves the underlying message, event, or state required for recovery.

If a chat message must remain available for 30 days, that requirement belongs in chat storage. It should not be implemented by setting a notification TTL and assuming the notification service is the archive.

The same separation makes reconnect logic deterministic:

push expired?       irrelevant to durable state
socket disconnected? irrelevant to durable state

client cursor = 120
server cursor = 127

sync -> events 121..127

Transport failure changes when the client observes state, not whether the authoritative state exists.

Idempotent synchronization absorbs duplicate signals

Wake-up signals can race with an already active connection. A user might receive an event over WebSocket just before an FCM notification causes another synchronization request.

The application should expect that overlap.

If events have stable identifiers or monotonic cursors, synchronization can be idempotent:

local cursor: 127

WebSocket event 128
  -> apply 128
  -> cursor = 128

FCM wake-up arrives
  -> sync after 128
  -> no duplicate domain event

Without this property, adding FCM as a fallback can create duplicate notifications, repeated inserts, or inconsistent unread counters. The failure is not caused by FCM itself; it comes from treating multiple delivery paths as if each were authoritative.

Choosing the live channel

Once FCM is separated into a wake-up role, the choice among WebSocket, MQTT, and SignalR becomes clearer.

WebSocket is a low-level fit when the application wants a bidirectional persistent channel and is prepared to define its own reconnect, routing, acknowledgement, and state synchronization semantics.

MQTT fits systems that benefit from standardized publish/subscribe semantics, broker-managed subscriptions, session behavior, retained messages, and selectable QoS.

SignalR fits applications that want a higher-level real-time programming model, especially in an ASP.NET Core stack, with connection management and transport selection handled by the framework.

None of those choices removes the need for durable state on a mobile application that must survive suspension and disconnection.

The architecture can therefore be expressed as two independent decisions:

How does an active client communicate?
    WebSocket / MQTT / SignalR / another live channel

How does an inactive mobile client get another chance to run?
    platform push service such as FCM

The synchronization protocol connects those decisions. Persistent connections optimize the path while the client is online. FCM restores an opportunity to communicate after that path disappears. Durable state, application acknowledgements, and idempotent synchronization are what make the two paths behave like one system.