We shipped a version of our Android SDK that crashed on every single boot, on certain phones, and passed every test we had.
The bug is worth writing up because it is not really ours. It is a property of the platform that catches any SDK maintaining a network connection, it is invisible on the hardware most developers test with, and the obvious fix is wrong.
What the platform actually requires
When you start a foreground service, Android gives you a short window — around
five seconds — to call startForeground() with a notification. Miss it and the
system kills the process with ForegroundServiceDidNotStartInTimeException.
That is well documented. What is less obvious is how easy it is to miss on a device that is not yours.
What we did wrong
Our service started, initialised the SDK, set up the engine, and then foregrounded. Reasonable-looking order: get the thing ready, then announce it.
On a modern phone, initialisation took a few hundred milliseconds and the window was never in danger. On a cheap device, cold, at boot, with a dozen other apps doing their own start-up work, initialisation ate the entire budget. The process was killed before it foregrounded. Every boot. On every device slow enough.
Three things made this worse than an ordinary ordering bug.
An emulator will not reproduce it
Emulators run on your development machine's CPU and finish cold initialisation well inside the window. Our full emulator matrix passed. It would have passed however wrong the ordering was.
If a vendor tells you they tested a background SDK on an emulator, they did not test this. Ask what physical hardware they use and how cold the boot was.
stopSelf() does not settle the debt
The natural defensive fix is to bail out early — if there is no configuration
yet, stop the service and return. That does not work. A service started as a
foreground service owes a startForeground() call even if it stops
immediately. Returning early without foregrounding is exactly as fatal as
taking too long, and it looks like the careful option.
Static configuration has no instance at boot
Our configure() was static, so an app calling it from an Activity had nothing
configured at boot — when no Activity has run. The service woke up, found no
config, and took the early-return path above. The two bugs combined into an
unconditional boot crash.
The rules that came out of it
Call startForeground() first. Before the SDK. Before the engine. Before the
network. Before any early return, any null check, any configuration read.
Build the notification so it works with nothing configured. If your notification construction depends on config that may not exist at boot, you have moved the crash rather than fixed it. A generic string is fine; a crash is not.
Assume the service can start with no Activity having run. Boot completion, job scheduler, restart after process death — none of them involve your UI.
Test on the slowest physical device you can find, from cold. Not from a warm start, not from the launcher. Reboot the phone.
Why a unit test cannot hold this
The deadline lives in the platform's ActivityManager. A JVM test has no
ActivityManager, so it cannot observe the window at all, and an instrumented
test on an emulator will not miss it.
What we ended up with is a source-structure check: a test that reads the
service source and asserts startForeground() appears before anything else in
the relevant paths. That is an unusual shape for a test and it is the correct one
here — the property being protected is an ordering in source, and the only
alternative is a device farm.
It covers both of our services, because there are two — the one integrators get and the one that ships in our own app — and each had the bug independently. Fixing one is not fixing the other, which is its own small lesson about shared cores with separate shells.
What to ask any vendor
This applies to any SDK that maintains a connection, not only ours.
- What happens on a cold boot on a low-end device?
- Do you call
startForeground()before initialisation, or after? - What happens if your service starts before the app has configured it?
- What physical hardware do you test on?
- Is there a test that would catch a regression in this ordering?
A vendor who has hit this will answer immediately and in detail, because it is memorable. A vague answer means they have not, which means you will.
The general lesson
The failure was invisible in every environment we controlled and unconditional in an environment we did not. That combination — passing tests plus a hardware-dependent crash — is the shape of most SDK integration pain, and it is why the advice in how to evaluate a passive income SDK ends with shipping to 1% of users and watching the crash rate for a week rather than with reading a feature list.
If you are weighing an Android integration more broadly, monetizing an Android app when you will not ship ads covers the platform decisions around it, and the Android page has our integration specifics.
Your first 100 devices go live today.
No sales call, no approval to start. Sign up, drop the SDK in, and watch the first device appear in your dashboard.
Create your free accountFrequently asked
What is the Android startForeground time limit?
Android gives a process roughly five seconds after starting a foreground service to call `startForeground()` with a notification. Missing that window causes the system to kill the process with `ForegroundServiceDidNotStartInTimeException`. The window is generous on fast hardware and easy to exhaust on a cold boot on a low-end device, which is why the failure is usually reported from the field rather than caught in testing.
Why does my foreground service crash only on some devices?
Almost always because initialisation runs before `startForeground()` and consumes the platform's start-up window. On a fast phone that initialisation takes a few hundred milliseconds and nothing goes wrong; on cheap hardware at cold boot, competing with other apps' start-up work, it can exceed five seconds and the process is killed. The fix is to call `startForeground()` first and initialise afterwards, with a notification that can be constructed before any configuration has loaded.
Does calling stopSelf() avoid the startForeground requirement?
No. A service started as a foreground service still owes a `startForeground()` call even if it stops immediately afterwards. Returning early without foregrounding — for example when configuration has not been set yet — is exactly as fatal as taking too long, and it is a common mistake precisely because it looks like the defensive option. Foreground first, then decide whether to stop.
Can an emulator reproduce the foreground service deadline?
No, and this is the trap. Emulators run on development-machine CPUs and complete cold initialisation well inside the window, so an emulator matrix passes regardless of how wrong the ordering is. Reproducing it requires a slow physical device booted from cold. When evaluating any SDK that runs a foreground service, ask specifically what physical hardware the vendor tests on.
How do you test for this in CI?
A source-structure check is the practical answer. The deadline is enforced by the platform's ActivityManager, which a JVM test does not have, and an instrumented test on an emulator will not miss the window. Reading the service source and asserting that `startForeground()` appears before initialisation and before any early return is an unusual shape for a test, but it protects an ordering that exists in source, and the only alternative is a physical device farm.