firebase / firebase/firebase-android-sdk
BLOCKING_EXECUTOR in ExecutorsRegistrar uses unbounded CachedThreadPool — causes 68+ threads on low-memory devices
- Dominant language
- Java
- Stars
- 2.6k
- Forks
- 710
- Avg merge
- 2d 9h
- Merged PRs (30d)
- 31
Description
## Description
`ExecutorsRegistrar.BLOCKING_EXECUTOR` uses `Executors.newCachedThreadPool()` with no upper bound on thread creation:
```java
// firebase-common/.../concurrent/ExecutorsRegistrar.java
static final Lazy BLOCKING_EXECUTOR =
new Lazy<>(
() ->
scheduled(
Executors.newCachedThreadPool(
factory("Firebase Blocking", ...))));
```
On low-end Android devices (2GB RAM, 2 CPU cores), this creates **68+ "Firebase Blocking Thread"** instances during normal app usage. Each thread consumes ~1MB of stack space, contributing to memory pressure that triggers the low memory killer (`lmkd`).
## Environment
- **Firebase product:** firebase-messaging 25.0.1 (only Firebase product in use; `BLOCKING_EXECUTOR` comes from the transitive `firebase-common` dependency)
- **Device:** 2GB RAM / 2-core CPU (emulator configured to simulate low-end Android Go devices)
- **Android version:** API 34
## Evidence
Thread dump captured via `Thread.getAllStackTraces()` shows 68 threads named "Firebase Blocking Thread #0" through "Firebase Blocking Thread #67", all in `TIMED_WAITING` state:
```
"Firebase Blocking Thread #0" TIMED_WAITING
"Firebase Blocking Thread #1" TIMED_WAITING
...
"Firebase Blocking Thread #67" TIMED_WAITING
```
Shortly after, the process was killed by `lmkd`:
```
lowmemorykiller: reason: low watermark is breached and swap is low (276kB < 151960kB)
ActivityManager: Process has died: fg TOP
```
The app was the **foreground TOP process** and was still killed due to system-wide memory exhaustion.
## Comparison
On a higher-spec emulator (4+ cores, more RAM), the same executor creates ~37 "Firebase Blocking Thread" instances. The slower CPU on the 2GB device means tasks take longer to complete, so more threads pile up before they can be reused — the exact scenario where unbounded pools are most dangerous.
## Suggested fix
Replace `Executors.newCachedThreadPool()` with a bounded `ThreadPoolExecutor` in `ExecutorsRegistrar.BLOCKING_EXECUTOR`:
```java
static final Lazy BLOCKING_EXECUTOR =
new Lazy<>(
() ->
scheduled(
new ThreadPoolExecutor(
0,
Math.max(4, Runtime.getRuntime().availableProcessors()),
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
factory("Firebase Blocking", ...))));
```
This caps thread creation while preserving the same idle-thread reaping behavior. A `LinkedBlockingQueue` alternative would allow tasks to queue safely when all threads are busy.
Contributor guide
Assessment
This issue has not been assessed yet.