firebase / firebase/firebase-android-sdk

BLOCKING_EXECUTOR in ExecutorsRegistrar uses unbounded CachedThreadPool — causes 68+ threads on low-memory devices

Open
#7,899 1 comment 0 reactions 0 assignees View on GitHub
api: core api: messaging
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.