Tutorials

    How Android Handles Push Notifications When the App is Killed

    A
    AzeemSep 2, 2026 · 9 min read
    133
    How Android Handles Push Notifications When the App is Killed

    Your app is completely killed. Swiped away from recent apps. Zero processes alive. Yet a notification still shows up instantly.

    How is this even possible?

    If you’re like most Android developers, you’ve used Firebase Cloud Messaging (FCM) countless times. But have you ever stopped to ask: what’s actually happening under the hood?

    In this deep-dive, we’ll uncover the complete architecture behind Android push notifications, why 90% of developers get this wrong in interviews, and how understanding this system can help you land that ₹15–25 LPA Android developer role.

    The Problem Most Developers Don’t Understand

    Here’s a scenario:

    You’re building a chat app. Users complain: “I only get messages when I open the app!”

    You check your code. FCM is integrated correctly. onMessageReceived() is implemented. Everything looks fine.

    But notifications still don’t work when the app is killed.

    The issue isn’t your code. It’s your understanding of how Android push notifications actually work.

    Companies like Google, Flipkart, PhonePe, and Swiggy expect senior Android developers (3–6 years experience, ₹15–28 LPA) to understand this at a system design level. Not just “add Firebase dependency and call the API.” Let’s fix that.

    The Architecture: It’s Not Your App Holding the Connection

    Here’s the truth that surprises most developers:
    Your app doesn’t maintain the connection to FCM servers. Google does.

    The Complete Flow:

    Your Backend Server
            ↓
    Firebase Cloud Messaging (FCM) Servers
            ↓
    Google Play Services (runs on every Android device)
            ↓
    Binder IPC (Inter-Process Communication)
            ↓
    Your App's FirebaseMessagingService
            ↓
    Notification Displayed
    code

    Key Components:

    1. Google Play Services

    • Runs as a system-level process (never killed by Android)

    • Maintains one persistent TCP/QUIC connection to FCM servers

    • Shared across ALL apps on the device

    • Has elevated OS privileges your app doesn’t have

    2. Binder IPC

    • Android’s mechanism for inter-process communication

    • Allows Google Play Services to wake your app’s process

    • Secure, efficient, kernel-level implementation

    3. FirebaseMessagingService

    • Your app’s service that receives messages

    • Runs in your app’s process (separate from Play Services)

    • Woken up by Android OS via Binder IPC when message arrives

    Why One Shared Connection? (Battery Life)

    Here’s the genius part:

    Android maintains ONE shared connection for ALL apps on the device.

    Not one per app. ONE.
    Think about it:

    • Average user has 50–100 apps installed

    • If each app maintained its own persistent connection

    • Each connection = radio wake-ups, keep-alive packets, battery drain

    • Your battery would die in 3–4 hours

    Instead:

    • Google Play Services holds one connection

    • All apps share it

    • Radio stays idle most of the time

    • Messages batched when possible

    • Result: Notifications without killing battery ⚡

    This is why Android push notifications work reliably even on budget devices with 4000mAh batteries lasting 1–2 days.

    The 3 App States (And How Notifications Work in Each)

    This is where most developers get confused. Let’s clarify:State 1: Foreground (App is Open and Active)

    What happens:

    • onMessageReceived() fires immediately

    • Your code runs

    • You control everything

    Notification payload:

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // This runs
        // You must manually display notification
        showNotification(...)
    }
    code

    Data payload:

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // This also runs
        // You must manually display notification
        showNotification(...)
    }
    code

    Key point: In foreground, system does NOT auto-display notifications. You handle everything.

    State 2: Background (App Minimized, Not Killed)

    What happens:

    • App is in background (home button pressed)

    • Your app process still exists

    • But not visible to user

    Notification payload:

    {
      "to": "FCM_TOKEN",
      "notification": {
        "title": "New Message",
        "body": "You have a new message"
      }
    }
    code

    Result:

    • System auto-displays notification in tray

    • onMessageReceived() does NOT fire

    • Your code doesn’t run at all

    • User taps notification → app opens

    Data payload:

    {
      "to": "FCM_TOKEN",
      "data": {
        "title": "New Message",
        "body": "You have a new message"
      }
    }
    code

    Result:

    • onMessageReceived() fires

    • You must manually display notification

    • System does NOT auto-display

    Pro tip: In background, notification payload = system handles it, data payload = you handle it.

    State 3: Killed (App Swiped Away, Process Dead)

    This is the one that confuses everyone.

    What happens:

    • App process is completely dead

    • No activities, services, or receivers running

    • User swiped it away from recent apps

    Notification payload:

    {
      "to": "FCM_TOKEN",
      "notification": {
        "title": "New Message",
        "body": "You have a new message"
      }
    }
    code

    Result:

    • System auto-displays notification in tray

    • Your app code does NOT run

    • onMessageReceived() does NOT fire

    • User taps notification → app launches from scratch

    Data payload:

    {
      "to": "FCM_TOKEN",
      "priority": "high",
      "data": {
        "title": "New Message",
        "body": "You have a new message"
      }
    }
    code

    Result:

    • Android OS wakes your FirebaseMessagingService via Binder IPC

    • onMessageReceived() fires (your code runs!)

    • You have ~10 seconds max before OS kills it again

    • You must manually display notification

    • Service completes → app goes back to killed state

    Critical: Data messages in killed state require "priority": "high". Without it, Android may delay delivery until user opens app. This breaks real-time features silently.

    Critical Caveats (Production Gotchas)

    1. Force-Stop = No Notifications

    If user force-stops your app from Settings:

    • Notifications stop completely

    • No workaround exists

    • User must manually open app once

    Why: This is intentional Android security. Force-stop means “I don’t want this app to run.” Android respects that.

    What to do: Nothing. This is by design.

    2. OEM Battery Optimization (Xiaomi, Huawei, Oppo, Vivo)

    Chinese OEMs have aggressive battery optimization:

    • MIUI (Xiaomi), EMUI (Huawei), ColorOS (Oppo), Funtouch OS (Vivo)

    • May block background wake-ups even with high priority

    • Users complain: “I only get notifications when I open the app!”

    Solution:

    // Request user to disable battery optimization
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        val powerManager = getSystemService(POWER_SERVICE) as PowerManager
        if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
            val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
                data = Uri.parse("package:$packageName")
            }
            startActivity(intent)
        }
    }
    code

    Also: Guide users to whitelist your app in battery settings.

    3. FCM Token Lifecycle

    FCM tokens are not permanent:

    • Change when app is reinstalled

    • Change when user clears app data

    • Rotated periodically by Google for security

    Production pattern:

    class MyFirebaseMessagingService : FirebaseMessagingService() {
        
        override fun onNewToken(token: String) {
            super.onNewToken(token)
            // Send new token to your backend
            sendTokenToBackend(token)
        }
    }
    code

    Always handle token refresh on your backend.

    4. 10-Second Service Limit

    In killed state, your FirebaseMessagingService has ~10 seconds max to complete.

    If your code takes longer:

    • Android kills it mid-execution

    • No callback, no error, just gone

    • Notification never displayed

    Solution:

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // Quick work only (display notification)
        showNotification(...)
        
        // Heavy work? Offload to WorkManager
        val workRequest = OneTimeWorkRequestBuilder<SyncWorker>().build()
        WorkManager.getInstance(this).enqueue(workRequest)
    }
    code

    Keep onMessageReceived() minimal. Offload heavy tasks to WorkManager.

    Notification vs Data Payload: When to Use What

    This is the decision that separates working demos from production apps:

    AspectNotification PayloadData PayloadForegroundonMessageReceived() firesonMessageReceived() firesBackgroundSystem displays (no code runs)onMessageReceived() firesKilledSystem displays (no code runs)onMessageReceived() firesControlLimited (system UI)Full (custom UI)Best ForStandard alertsCustom UI, deep linking

    Production Pattern:

    Use Notification Payload for:

    • Chat messages

    • Social media notifications

    • News updates

    • Reminders and alerts

    • Standard use cases (80% of scenarios)

    Use Data Payload for:

    • Custom notification UI (rich notifications)

    • Deep linking to specific screens

    • Silent background sync

    • VOIP calls (incoming call screens)

    • When you need full control (20% of scenarios)

    Most production apps use a mix of both, deliberately not by accident.

    Complete Implementation Example

    Here’s production-ready code:

    FirebaseMessagingService:

    class MyFirebaseMessagingService : FirebaseMessagingService() {
        
        override fun onMessageReceived(remoteMessage: RemoteMessage) {
            Log.d("FCM", "Message received from: ${remoteMessage.from}")
            
            // Notification payload (only called in foreground)
            remoteMessage.notification?.let { notification ->
                showNotification(
                    title = notification.title ?: "Notification",
                    body = notification.body ?: "",
                    data = remoteMessage.data
                )
            }
            
            // Data payload (called in all states)
            if (remoteMessage.data.isNotEmpty()) {
                // In background/killed, must manually show notification
                if (!isAppInForeground()) {
                    showNotification(
                        title = remoteMessage.data["title"] ?: "Notification",
                        body = remoteMessage.data["body"] ?: "",
                        data = remoteMessage.data
                    )
                }
                
                // Offload heavy work to WorkManager
                val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
                    .setInputData(workDataOf("messageId" to remoteMessage.data["messageId"]))
                    .build()
                
                WorkManager.getInstance(this).enqueue(workRequest)
            }
        }
        
        override fun onNewToken(token: String) {
            super.onNewToken(token)
            // Send new token to backend
            sendTokenToBackend(token)
        }
        
        private fun showNotification(title: String, body: String, data: Map<String, String>) {
            val channelId = "default_channel"
            
            // Deep link intent
            val intent = Intent(this, MainActivity::class.java).apply {
                putExtra("messageId", data["messageId"])
                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            }
            
            val pendingIntent = PendingIntent.getActivity(
                this, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )
            
            val notification = NotificationCompat.Builder(this, channelId)
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(title)
                .setContentText(body)
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
                .build()
            
            val notificationManager = NotificationManagerCompat.from(this)
            notificationManager.notify(System.currentTimeMillis().toInt(), notification)
        }
        
        private fun isAppInForeground(): Boolean {
            val appProcessInfo = RunningAppProcessInfo()
            ActivityManager.getMyMemoryState(appProcessInfo)
            return appProcessInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND
        }
        
        private fun sendTokenToBackend(token: String) {
            // Send to your backend server
            // Update user's FCM token in database
        }
    }
    code

    AndroidManifest.xml:

    <manifest>
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
        
        <application>
            <service
                android:name=".MyFirebaseMessagingService"
                android:exported="false">
                <intent-filter>
                    <action android:name="com.google.firebase.MESSAGING_EVENT" />
                </intent-filter>
            </service>
            
            <meta-data
                android:name="com.google.firebase.messaging.default_notification_channel_id"
                android:value="default_channel" />
                
            <meta-data
                android:name="com.google.firebase.messaging.default_notification_icon"
                android:resource="@drawable/ic_notification" />
        </application>
    </manifest>
    code

    Interview Questions (Test Your Understanding)

    If you can answer these, you’re ahead of 90% of Android candidates:

    Q1: Why doesn’t Android give each app its own persistent connection to FCM?

    Answer: Battery life. One shared connection for all apps = efficient radio usage, less battery drain.

    Q2: What happens when a user force-stops your app from Settings?

    Answer: Notifications stop completely until user manually opens app. No workaround (intentional security feature).

    Q3: Why do data messages in killed state require "priority": "high"?

    Answer: Without high priority, Android may delay delivery until user opens app (breaks real-time features).

    Q4: What’s the time limit for FirebaseMessagingService in killed state?

    Answer: ~10 seconds max. After that, Android kills it mid-execution.

    Q5: How does Google Play Services communicate with your app?

    Answer: Binder IPC (Inter-Process Communication) across process boundaries.

    Q6: When does onMessageReceived() fire vs when does system auto-display?

    Answer:

    • Foreground: Always fires; you display

    • Background + notification payload: System displays, doesn’t fire

    • Background + data payload: Fires, you display

    • Killed + notification payload: System displays, doesn’t fire

    • Killed + data payload: Fires, you display

    🚀 Want to Master Android & Land ₹30+ LPA?

    I’m launching an exclusive Android Job-Ready Bootcamp.

    What You Get:
    ✅ Kotlin, Jetpack Compose, MVVM, Clean Architecture, System Design, Machine coding
    ✅ Android Internals + Mock Interviews (FAANG engineers)
    ✅ Step-by-step roadmap and guide to crack Senior Android roles
    ✅ Direct Referrals to Top Companies

    Limited Seats. Serious Developers Only.

    DM on LinkedIn: 👉 linkedin.com/in/azimsidd
    Explore Curriculum: 🌐 thecodingshef.in/course/master-android

    Your pathway to ₹30+ LPA starts here.

    Key Takeaways

    1. Google Play Services maintains one shared connection for all apps (not your app)

    2. Binder IPC allows Play Services to wake your app when notifications arrive

    3. 3 app states behave differently: foreground (you handle), background (system or you), killed (system or brief wake-up)

    4. Notification payload = system displays, Data payload = you display

    5. Force-stop = no notifications (intentional, no workaround)

    6. OEM restrictions (Xiaomi, Huawei) can block wake-ups (guide users to disable)

    7. FCM tokens change (handle refresh on backend)

    8. 10-second limit in killed state (keep work minimal, offload to WorkManager)

    Save This Article

    Bookmark this for:

    • Your next FCM debugging session

    • Android system design interview prep

    • Understanding production notification issues

    • Teaching your team how FCM actually works

    android
    kotlin
    app development
    programming
    mobile app development
    android developers
    Was this helpful?

    Comments (0)

    No comments yet. Be the first to share your thoughts!

    Leave a Comment

    Get the latest updates

    Join our newsletter and never miss an article.

    By subscribing, you agree to our Privacy Policy and consent to receive updates.

    Ready to Start Learning?

    Join thousands of students building their dream careers

    Explore Courses