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 /
SignalRWhen 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 stateThe 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
serverIf 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 m42FCM 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 channelWhile 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 processMQTT 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 clientsBoth 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..127Transport 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 eventWithout 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 FCMThe 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.