Repository navigation
[ANR] FCMBroadcastReceiver goAsync starved by 2-thread IO pool during init HTTP (android_params.js / sdk/features) #2722
Description
Summary
FCMBroadcastReceiver ANRs on Android when a push arrives while OneSignal’s 2-thread IO pool is blocked on init HTTP to api.onesignal.com (android_params.js and sdk/features). goAsync() does not extend the ~10s broadcast timeout, so pendingResult.finish() never runs in time.
This is not a main-thread Application.onCreate stall. initWithContext returns in ~100ms on the main thread; the ANR is the broadcast watchdog.
Related: #2709 (open). Play Vitals data there showed 5.9.5 prewarm did not reduce FCMBroadcastReceiver ANR share. That issue attributes the problem to dispatcher construction race. Our traces show the dispatchers are already warm and both IO workers are stuck in TCP connect — pool saturation + timeout mismatch (and on many devices, IPv6 with no Happy Eyeballs).
Also related (different root cause, same symptom string): #1542, #1436, #2324, #2619.
Environment
| SDK | 5.9.8 and 5.9.9 (SDK-Version=onesignal/android/050908 and 050909). Also reproduced under Gradle dynamic range [5.6.1, 5.9.99]. |
| Device | Xiaomi Mi 10 (umi), Android 13 / MIUI |
| App | Process started by FCM or already in foreground; OneSignal.initWithContext(applicationContext, appId) from Application.onCreate |
| Network | China Mobile; device has a global IPv6 address; api.onesignal.com has AAAA (Cloudflare) |
Repro
- Force-stop the app (or wait until the process is dead).
- Send an FCM / OneSignal push so Play services delivers
com.google.android.c2dm.intent.RECEIVE(or open the app and send a push in the first ~60s while init HTTP is still in flight). - Within ~10s: ANR
Broadcast of Intent { act=com.google.android.c2dm.intent.RECEIVE cmp=.../com.onesignal.notifications.receivers.FCMBroadcastReceiver }.
Warm process after init HTTP has finished: later pushes usually do not ANR.
Timeline (5.9.8, one occurrence)
Times local GMT+8, pid 20919:
| Time | Event |
|---|---|
| 16:19:03 | OneSignalImp initialization completed on main (~96ms) |
| 16:19:03–04 | GET .../android_params.js on OneSignal-IO-2; GET .../sdk/features/android/050908 on OneSignal-IO-1 |
| ~16:19:24 | FCM google.sent_time |
| 16:19:35 | System ANR (broadcast ~10s) |
| 16:20:08 | Both HTTP responses (~64s); then NotificationWorkManager enqueueing notification work |
Same pattern on 5.9.9 with the app already in the foreground: init finished in 132ms; both GETs still connecting; Timeout of broadcast ... started 10001ms ago → ANR.
What the ANR dump shows
Main thread: typically idle (MessageQueue.nativePollOnce). Sometimes unrelated Compose draw. Not blocked in OneSignal.
OneSignal-IO-1 **and **OneSignal-IO-2 (both Native / poll):
at libcore.io.Linux.poll
at libcore.io.IoBridge.isConnected
at libcore.io.IoBridge.connectErrno
at java.net.PlainSocketImpl.socketConnect
at java.net.Socket.connect
at com.android.okhttp.internal.io.RealConnection.connectSocket
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponseCode
at com.onesignal.core.internal.http.impl.HttpClient$makeRequestIODispatcher$job$1.invokeSuspend(HttpClient.kt:206)
at com.onesignal.common.threading.ThreadUtilsKt$launchOnIO$1.invokeSuspend
at com.onesignal.common.threading.OneSignalDispatchers$launchOnIO$1.invokeSuspend(OneSignalDispatchers.kt:202)
SDK code (5.9.9) that matches this
FCMBroadcastReceiver.onReceive:OneSignalDispatchers.prewarm()thengoAsync()thensuspendifyOnIO { initWithContext; processBundle; pendingResult.finish() }.OneSignalDispatchers:ThreadPoolExecutor(core=2, max=3, LinkedBlockingQueue(200)). JDK semantics: queue fills before max, so effective concurrency is 2.ConfigModel.httpGetTimeoutdefault 60000ms;HttpClientapplies it to connect and read timeout. One hung GET holds an IO thread for up to 60s.- Cold start fires two GETs at once (
android_params.js+sdk/features) → both IO threads busy. - Broadcast budget is ~10s.
goAsync()does not extend it (Chronic background ANRs (FCMBroadcastReceiver, NotificationDismissReceiver, BootUpReceiver, SyncJobService) unchanged after 5.9.5 prewarm (PR #2664) — version-code data #2709). 60s vs 10s.
sequenceDiagram
participant Play as PlayServices
participant Rx as FCMBroadcastReceiver
participant IO as OneSignal_IO_pool_2
participant API as api.onesignal.com
Note over IO: GET android_params.js
Note over IO: GET sdk/features
IO->>API: TCP connect (can hang 60s)
Play->>Rx: c2dm RECEIVE
Rx->>Rx: goAsync
Rx->>IO: suspendifyOnIO processBundle
Note over IO: queued, both workers in connect
Note over Rx: 10s, finish never called, ANR
IPv6 / Happy Eyeballs (why connect hangs)
Same device, same URL, same session:
| Client | Result |
|---|---|
curl (Happy Eyeballs, IPv4 fallback) |
connect ~0.3s, HTTP 200, total <1s |
curl -6 |
fails after ~15s (code=000) |
SDK HttpURLConnection |
same request in-flight 10–64s; ANR dump in IoBridge.connectErrno |
- Device has a global IPv6 address.
api.onesignal.comhas AAAA (Cloudflare). IPv6 path to that anycast is broken on this carrier.- Java
HttpURLConnection/InetAddresshas no Happy Eyeballs (RFC 8305); it serializes addresses and sits on IPv6 untilconnectTimeout(60s). System.setProperty("java.net.preferIPv4Stack", "true")inApplication.attachBaseContextdid not change SDK connect behavior on this device.
This also fits #2709’s device mix (emerging-market OEMs where IPv6 is on but Cloudflare IPv6 routing is poor).
What we already ruled out (app side)
- Main-thread
OneSignal.User/ feature-gaterunBlocking(separate ANR class; we moved those reads off main — main is idle in these dumps). - Native
POST_NOTIFICATIONSvsOneSignal.Notifications.requestPermission. - R8 / missing
-keep(staging/debug, minify off; stacks show full OneSignal names). UncaughtExceptionHandler(this is a watchdog timeout, not a crash).- Downgrade-only experiments: 5.9.8 reproduced; 5.9.9 reproduced. [Bug]: One Signal causing too many ANRS #2619 also reported the same broadcast ANR string on 5.7.7 (different inner stack).
- App
FirebaseMessagingService: Play still deliversc2dmto OneSignal’s receiver at priority 999 first.
Requested fix
Any of these would close the 10s window:
- Do not share the 2-thread IO pool with
goAsyncwork. Use a dedicated thread/pool (orDispatchers.IOunbounded) soprocessBundle+finish()cannot wait behind 60s connects. - **Enqueue WorkManager then **
finish()immediately — you already haveNotificationWorkManager; today enqueue still sits behind the hung GETs. - Happy Eyeballs or IPv4-first for
HttpURLConnection(or switch the SDK HTTP stack to OkHttp, which does HE). - Much shorter connect timeout for init GETs (seconds, not 60s), without blocking the FCM receiver on those calls.
prewarm() only helps pool construction, not workers stuck in connect().
Happy to attach a truncated dropbox excerpt (IO threads only) if useful.
Activity
TairineAlvesTW commentedon Sep 16, 2026
We reproduced this on a physical device with a full ANR dump, and it matches the description here:
FCMBroadcastReceiverstarved because bothOneSignal-IOthreads are blocked in an HTTP connect.Setup
sdkVersion=051000in logcat), .NET MAUI 10 appping6to the OneSignal host times out, IPv4 works)What happened
FCMBroadcastReceiverhit the 10 s foreground broadcast timeout and Android showed the "app isn't responding" dialog.ANR dump (Subject: Broadcast of Intent ... FCMBroadcastReceiver, Foreground: Yes)
main: idle inMessageQueue.nativePollOnceOneSignal-IO-1andOneSignal-IO-2: both blocked in a socket connect from the SDK HTTP client:With the whole 2-thread IO pool waiting on connects that go to IPv6 first, the receiver work queued behind them cannot run before the broadcast deadline.
Production impact
In Play Console this is our largest ANR cluster:
FCMBroadcastReceiver/nativePollOnce, 70 users over 28 days on one app, still present on our current release. Networks with broken IPv6 are not rare for our users in Brazil (home routers and some ISPs), so this is not a lab-only case.As a mitigation on our side we stopped sending the "order placed" push, which reached users while the app was open. That only reduces exposure. The receiver itself still depends on the IO pool being free.
Questions
FCMBroadcastReceiveravoid waiting on the shared IO pool, or run on a separate executor, so pending HTTP calls can't starve it?Happy to share the full thread dump or test a build if that helps.