The silent-notification trap: why a driver swears the phone stayed quiet
A driver insisted the phone never made a sound for a job worth real money, and he was right: the OS had quietly demoted my correct notification to silent.
A driver swore his phone stayed dead silent through a job that paid real money, and the infuriating part is that he was telling the truth. My notification fired. The server logs proved it left, the device logs proved it arrived. Somewhere in the gap between “delivered” and “the human heard it,” the operating system took my correct, well-formed alert and decided, on its own authority, to mute it.
That is the trap. You think push is a thing your code does. On a time-critical event, push is a thing you negotiate with a hostile OS that assumes you are spam until proven otherwise.
All the ways a budget phone eats your alert
I shipped the textbook version first: a standard FCM message with a notification payload, a title, a body, a sound. It works perfectly on a clean test device sitting on your desk. It falls apart in the field, and every failure is invisible from your dashboard because the message was, technically, delivered.
- It gets collapsed. Send two alerts close together and the system folds them into one. The driver who missed the first never even gets a second chance.
- It gets throttled and demoted. On a budget device, the OS watches how often your channel fires and quietly drops its importance to silent. No sound, no heads-up, just a line that appears if he happens to look.
- It gets killed by battery optimization. The app is swiped away or asleep, the aggressive power manager has frozen it, and your message lands in a process that is no longer allowed to do anything about it.
None of this is a bug in your code. It is the platform doing exactly what it was designed to do to apps it doesn’t trust. The default notification path is built for “you have a new like,” not “income is leaving in ninety seconds.”
Stop asking, start owning the alert
The fix is to stop sending a notification and start sending an instruction. You send a data-only, high-priority message: no notification payload at all, just a quiet packet of fields. The OS has nothing pretty to mangle, so it hands the raw data to your code, and now you build the alert yourself, on your terms.
And “your terms” means treating it like an alarm clock, not a chime.
- Full-screen-intent. It takes over the locked screen the way an incoming call does, instead of waiting politely in a tray.
- A USAGE_ALARM channel. Put the alert on an alarm-class channel so it is audible even when the ringer is at zero, because a muted phone is the normal state of a phone in a pocket.
- A looping sound and vibration. It plays until acknowledged. A single chirp is a chirp he’ll miss; a loop is a thing he has to deal with.
A notification asks the OS to please make a sound. An alarm tells the OS it is going to make a sound. For anything that costs the user money to miss, you want the second one.
The loose ends nobody warns you about
Owning the alert means owning the plumbing too, and full-screen-intent has a nasty surprise: it gives you no tap callback. When the user finally interacts, your code never hears about it the normal way. So you drop a marker in local storage the moment you fire, and check that marker when the relevant screen opens, because that is the only thread connecting “I alarmed” to “they responded.”
Then there is the unglamorous tail. You need the runtime notification permission or you are shouting into the void on newer devices. You need deduplication, because high-priority delivery means the same event can arrive more than once and you do not want to alarm twice for one job. And you need stale-token handling, because a phone that reinstalls hands you a dead address that silently swallows everything.
None of it is hard. All of it is invisible until a real driver, on a real budget phone, swears the thing never made a sound. He’s not lying. He’s just describing what the OS did to the alert you thought you’d sent.